diff --git a/.brazil.json b/.brazil.json index 69638fc11a0a..0a9a13d4bfd9 100644 --- a/.brazil.json +++ b/.brazil.json @@ -1,171 +1,517 @@ { - "packageVersion": "2.0", - - "modules": { - "annotations": { "packageName": "AwsJavaSdk-Core-Annotations" }, - "apache-client": { "packageName": "AwsJavaSdk-HttpClient-ApacheClient" }, - "apache5-client": { "packageName": "AwsJavaSdk-HttpClient-Apache5Client" }, - "arns": { "packageName": "AwsJavaSdk-Core-Arns" }, - "auth": { "packageName": "AwsJavaSdk-Core-Auth" }, - "auth-crt": { "packageName": "AwsJavaSdk-Core-AuthCrt" }, - "aws-cbor-protocol": { "packageName": "AwsJavaSdk-Core-AwsCborProtocol" }, - "aws-core": { "packageName": "AwsJavaSdk-Core-AwsCore" }, - "aws-crt-client": { "packageName": "AwsJavaSdk-HttpClient-CrtClient" }, - "aws-ion-protocol": { "packageName": "AwsJavaSdk-Core-AwsIonProtocol" }, - "aws-json-protocol": { "packageName": "AwsJavaSdk-Core-AwsJsonProtocol" }, - "aws-query-protocol": { "packageName": "AwsJavaSdk-Core-AwsQueryProtocol" }, - "aws-xml-protocol": { "packageName": "AwsJavaSdk-Core-AwsXmlProtocol" }, - "smithy-rpcv2-protocol": { "packageName": "AwsJavaSdk-Core-SmithyRpcV2Protocol" }, - "cloudwatch-metric-publisher": { "packageName": "AwsJavaSdk-MetricPublisher-CloudWatch" }, - "emf-metric-logging-publisher": { "packageName": "AwsJavaSdk-MetricPublisher-Emf" }, - "codegen": { "packageName": "AwsJavaSdk-Codegen" }, - "dynamodb-enhanced": { "packageName": "AwsJavaSdk-DynamoDb-Enhanced" }, - "http-client-spi": { "packageName": "AwsJavaSdk-HttpClient" }, - "iam-policy-builder": { "packageName": "AwsJavaSdk-Iam-PolicyBuilder" }, - "json-utils": { "packageName": "AwsJavaSdk-Core-JsonUtils" }, - "metrics-spi": { "packageName": "AwsJavaSdk-Core-MetricsSpi" }, - "endpoints-spi": { "packageName": "AwsJavaSdk-Core-EndpointsSpi" }, - "netty-nio-client": { "packageName": "AwsJavaSdk-HttpClient-NettyNioClient" }, - "profiles": { "packageName": "AwsJavaSdk-Core-Profiles" }, - "protocol-core": { "packageName": "AwsJavaSdk-Core-ProtocolCore" }, - "regions": { "packageName": "AwsJavaSdk-Core-Regions" }, - "s3-transfer-manager": { "packageName": "AwsJavaSdk-S3-TransferManager" }, - "s3-event-notifications": { "packageName": "AwsJavaSdk-S3-EventNotifications" }, - "sns-message-manager": { "packageName": "AwsJavaSdk-Sns-MessageManager" }, - "sdk-core": { "packageName": "AwsJavaSdk-Core" }, - "url-connection-client": { "packageName": "AwsJavaSdk-HttpClient-UrlConnectionClient" }, - "utils": { "packageName": "AwsJavaSdk-Core-Utils" }, - "utils-lite": { "packageName": "AwsJavaSdk-Core-UtilsLite" }, - "imds": { "packageName": "AwsJavaSdk-Imds" }, - "crt-core": { "packageName": "AwsJavaSdk-Core-CrtCore" }, - "checksums-spi": { "packageName": "AwsJavaSdk-Core-ChecksumsSpi" }, - "checksums": { "packageName": "AwsJavaSdk-Core-Checksums" }, - "identity-spi": { "packageName": "AwsJavaSdk-Core-IdentitySpi" }, - "http-auth-spi": { "packageName": "AwsJavaSdk-Core-HttpAuthSpi" }, - "http-auth": { "packageName": "AwsJavaSdk-Core-HttpAuth" }, - "http-auth-aws": { "packageName": "AwsJavaSdk-Core-HttpAuthAws" }, - "http-auth-aws-crt": { "packageName": "AwsJavaSdk-Core-HttpAuthAwsCrt" }, - "http-auth-aws-eventstream": { "packageName": "AwsJavaSdk-Core-HttpAuthAwsEventStream" }, - "retries-spi": { "packageName": "AwsJavaSdk-Core-RetriesSpi" }, - "retries": { "packageName": "AwsJavaSdk-Core-Retries" }, - - "dynamodb": { "packageName": "AwsJavaSdk-DynamoDb" }, - "waf": { "packageName": "AwsJavaSdk-Waf" }, - "v2-migration": { "packageName": "AwsJavaSdk-Tooling-V2Migration"}, - - "third-party-jackson-core": { - "packageName": "AwsJavaSdk-ThirdParty-JacksonCore", - "artifactType": "JAR", - "includes": ["target/aws-sdk-java-third-party-jackson-core-*.jar"], - "excludes": ["aws-sdk-java-third-party-jackson-core-*-javadoc.jar"] - }, - "third-party-jackson-dataformat-cbor": { - "packageName": "AwsJavaSdk-ThirdParty-JacksonDataformatCbor", - "artifactType": "JAR", - "includes": ["target/aws-sdk-java-third-party-jackson-dataformat-cbor-*.jar"], - "excludes": ["aws-sdk-java-third-party-jackson-dataformat-cbor-*-javadoc.jar"] - }, - "ruleset-testing-core": { - "packageName": "AwsJavaSdk-Test-RuleSetTestingCore" - }, - - "archetype-app-quickstart": { "skipImport": true }, - "archetype-lambda": { "skipImport": true }, - "archetype-tools": { "skipImport": true }, - "archetypes": { "skipImport": true }, - "auth-tests": { "skipImport": true }, - "aws-sdk-java": { "skipImport": true }, - "aws-sdk-java-pom": { "skipImport": true }, - "bom": { "skipImport": true }, - "bom-internal": { "skipImport": true }, - "build-tools": { "skipImport": true }, - "bundle": { "skipImport": true }, - "bundle-sdk": { "skipImport": true }, - "bundle-logging-bridge": { "skipImport": true }, - "bundle-logging-bridge-binding-test": { "skipImport": true }, - "codegen-generated-classes-test": { "skipImport": true }, - "codegen-lite": { "skipImport": true }, - "codegen-lite-maven-plugin": { "skipImport": true }, - "codegen-maven-plugin": { "skipImport": true }, - "core": { "skipImport": true }, - "dynamodbdocument-v1": { "skipImport": true }, - "dynamodbmapper-v1": { "skipImport": true }, - "http-client-tests": { "skipImport": true }, - "http-clients": { "skipImport": true }, - "metric-publishers": { "skipImport": true }, - "module-path-tests": { "skipImport": true }, - "old-client-version-compatibility-test": { "skipImport": true }, - "protocol-tests": { "skipImport": true }, - "protocol-tests-core": { "skipImport": true }, - "protocols": { "skipImport": true }, - "region-testing": { "skipImport": true }, - "release-scripts": { "skipImport": true }, - "s3-benchmarks": { "skipImport": true }, - "http-client-benchmarks": { "skipImport": true }, - "sdk-benchmarks": { "skipImport": true }, - "sdk-standard-benchmarks": { "skipImport": true }, - "sdk-native-image-test": { "skipImport": true }, - "service-test-utils": { "skipImport": true }, - "services": { "skipImport": true }, - "services-custom": { "skipImport": true }, - "stability-tests": { "skipImport": true }, - "test-utils": { "skipImport": true }, - "tests-coverage-reporting": { "skipImport": true }, - "third-party": { "skipImport": true }, - "third-party-slf4j-api": { "skipImport": true }, - "v2-migration-tests": {"skipImport": true}, - "crt-unavailable-tests": { "skipImport": true }, - "bundle-shading-tests": { "skipImport": true }, - "architecture-tests": {"skipImport": true}, - "s3-tests": {"skipImport": true} - }, - - "dependencies": { - "com.fasterxml.jackson.core:jackson-annotations": { "packageName": "Jackson-annotations", "packageVersion": "2.9.x" }, - "com.fasterxml.jackson.core:jackson-core": { "packageName": "Jackson-core", "packageVersion": "2.9.x" }, - "com.fasterxml.jackson.core:jackson-databind": { "packageName": "Jackson-databind", "packageVersion": "2.12.x" }, - "com.fasterxml.jackson.datatype:jackson-datatype-jdk8": { "packageName": "Jackson-datatype-jdk8", "packageVersion": "2.12.x" }, - "com.fasterxml.jackson.dataformat:jackson-dataformat-cbor": { "packageName": "Jackson-dataformat-cbor", "packageVersion": "2.9.x" }, - "com.fasterxml.jackson.jr:jackson-jr-objects": { "packageName": "Maven-com-fasterxml-jackson-jr_jackson-jr-objects", "packageVersion": "2.11.x" }, - "com.fasterxml.jackson.jr:jackson-jr-stree": { "packageName": "Maven-com-fasterxml-jackson-jr_jackson-jr-stree", "packageVersion": "2.11.x" }, - "com.jayway.jsonpath:json-path": { "packageName": "JMESPathJava", "packageVersion": "1.0" }, - "com.squareup:javapoet": { "packageName": "Maven-com-squareup_javapoet", "packageVersion": "1.13.x" }, - "com.typesafe.netty:netty-reactive-streams": { "packageName": "Netty4", "packageVersion": "4.1" }, - "com.typesafe.netty:netty-reactive-streams-http": { "packageName": "Maven-com-typesafe-netty_netty-reactive-streams-http", "packageVersion": "2.x" }, - "commons-codec:commons-codec": { "packageName": "JakartaCommons-codec", "packageVersion": "1.x" }, - "io.netty:netty-buffer": { "packageName": "Netty4", "packageVersion": "4.1" }, - "io.netty:netty-codec": { "packageName": "Netty4", "packageVersion": "4.1" }, - "io.netty:netty-codec-http": { "packageName": "Netty4", "packageVersion": "4.1" }, - "io.netty:netty-codec-http2": { "packageName": "Netty4", "packageVersion": "4.1" }, - "io.netty:netty-common": { "packageName": "Netty4", "packageVersion": "4.1" }, - "io.netty:netty-handler": { "packageName": "Netty4", "packageVersion": "4.1" }, - "io.netty:netty-resolver": { "packageName": "Netty4", "packageVersion": "4.1" }, - "io.netty:netty-resolver-dns": { "packageName": "Netty4", "packageVersion": "4.1" }, - "io.netty:netty-transport": { "packageName": "Netty4", "packageVersion": "4.1" }, - "io.netty:netty-transport-classes-epoll": { "packageName": "Netty4", "packageVersion": "4.1" }, - "io.netty:netty-transport-native-unix-common": { "packageName": "Netty4", "packageVersion": "4.1" }, - "org.apache.httpcomponents:httpclient": { "packageName": "Apache-HttpComponents-HttpClient", "packageVersion": "4.5.x" }, - "org.apache.httpcomponents.client5:httpclient5": { "packageName": "Apache-HttpComponents-HttpClient5", "packageVersion": "5.0.x" }, - "org.apache.httpcomponents:httpcore": { "packageName": "Apache-HttpComponents-HttpCore", "packageVersion": "4.4.x" }, - "org.apache.httpcomponents.core5:httpcore5": { "packageName": "Apache-HttpComponents-HttpCore5", "packageVersion": "5.0.x" }, - "org.eclipse.jdt:org.eclipse.jdt.core": { "packageName": "AwsJavaSdk-Codegen-EclipseJdtDependencies", "packageVersion": "2.0" }, - "org.eclipse.text:org.eclipse.text": { "packageName": "AwsJavaSdk-Codegen-EclipseJdtDependencies", "packageVersion": "2.0" }, - "org.reactivestreams:reactive-streams": { "packageName": "Maven-org-reactivestreams_reactive-streams", "packageVersion": "1.x" }, - "org.slf4j:slf4j-api": { "packageName": "Slf4j", "packageVersion": "1.7" }, - "software.amazon.awssdk.crt:aws-crt": { "packageName": "Aws-crt-java", "packageVersion": "1.0.x" }, - "software.amazon.eventstream:eventstream": { "packageName": "AwsFlowJava", "packageVersion": "1.0" }, - "software.amazon.ion:ion-java": { "packageName": "Maven-software-amazon-ion_ion-java", "packageVersion": "1.x" }, - "software.amazon:flow": { "packageName": "AwsFlowJava", "packageVersion": "1.0" }, - "org.junit.jupiter:junit-jupiter": { "packageName": "JUnit5", "packageVersion": "5.x" }, - "org.mockito:mockito-core": { "packageName": "Mockito", "packageVersion": "3.x" }, - "io.reactivex.rxjava3:rxjava": { "packageName": "RxJava", "packageVersion": "3.x" }, - "org.assertj:assertj-core": { "packageName": "AssertJ", "packageVersion": "3.22.x" }, - "org.openrewrite:rewrite-java-8": {"packageName": "Maven-org-openrewrite_rewrite-java-8", "packageVersion": "8.x"}, - "org.openrewrite:rewrite-java": {"packageName": "Maven-org-openrewrite_rewrite-java", "packageVersion": "8.x"}, - "org.openrewrite.recipe:rewrite-java-dependencies": {"packageName": "Maven-org-openrewrite-recipe_rewrite-java-dependencies", "packageVersion": "1.x"} - }, - - "test-dependencies": { - "s3": ["JUnit5-5.x", "Aws-crt-java-1.0.x"] + "packageVersion": "2.0", + "modules": { + "annotations": { + "packageName": "AwsJavaSdk-Core-Annotations" + }, + "apache-client": { + "packageName": "AwsJavaSdk-HttpClient-ApacheClient" + }, + "apache5-client": { + "packageName": "AwsJavaSdk-HttpClient-Apache5Client" + }, + "arns": { + "packageName": "AwsJavaSdk-Core-Arns" + }, + "auth": { + "packageName": "AwsJavaSdk-Core-Auth" + }, + "auth-crt": { + "packageName": "AwsJavaSdk-Core-AuthCrt" + }, + "aws-cbor-protocol": { + "packageName": "AwsJavaSdk-Core-AwsCborProtocol" + }, + "aws-core": { + "packageName": "AwsJavaSdk-Core-AwsCore" + }, + "aws-crt-client": { + "packageName": "AwsJavaSdk-HttpClient-CrtClient" + }, + "aws-ion-protocol": { + "packageName": "AwsJavaSdk-Core-AwsIonProtocol" + }, + "aws-json-protocol": { + "packageName": "AwsJavaSdk-Core-AwsJsonProtocol" + }, + "aws-query-protocol": { + "packageName": "AwsJavaSdk-Core-AwsQueryProtocol" + }, + "aws-xml-protocol": { + "packageName": "AwsJavaSdk-Core-AwsXmlProtocol" + }, + "smithy-rpcv2-protocol": { + "packageName": "AwsJavaSdk-Core-SmithyRpcV2Protocol" + }, + "cloudwatch-metric-publisher": { + "packageName": "AwsJavaSdk-MetricPublisher-CloudWatch" + }, + "emf-metric-logging-publisher": { + "packageName": "AwsJavaSdk-MetricPublisher-Emf" + }, + "codegen": { + "packageName": "AwsJavaSdk-Codegen" + }, + "dynamodb-enhanced": { + "packageName": "AwsJavaSdk-DynamoDb-Enhanced" + }, + "http-client-spi": { + "packageName": "AwsJavaSdk-HttpClient" + }, + "iam-policy-builder": { + "packageName": "AwsJavaSdk-Iam-PolicyBuilder" + }, + "json-utils": { + "packageName": "AwsJavaSdk-Core-JsonUtils" + }, + "metrics-spi": { + "packageName": "AwsJavaSdk-Core-MetricsSpi" + }, + "endpoints-spi": { + "packageName": "AwsJavaSdk-Core-EndpointsSpi" + }, + "netty-nio-client": { + "packageName": "AwsJavaSdk-HttpClient-NettyNioClient" + }, + "profiles": { + "packageName": "AwsJavaSdk-Core-Profiles" + }, + "protocol-core": { + "packageName": "AwsJavaSdk-Core-ProtocolCore" + }, + "regions": { + "packageName": "AwsJavaSdk-Core-Regions" + }, + "s3-transfer-manager": { + "packageName": "AwsJavaSdk-S3-TransferManager" + }, + "s3-event-notifications": { + "packageName": "AwsJavaSdk-S3-EventNotifications" + }, + "sns-message-manager": { + "packageName": "AwsJavaSdk-Sns-MessageManager" + }, + "sdk-core": { + "packageName": "AwsJavaSdk-Core" + }, + "url-connection-client": { + "packageName": "AwsJavaSdk-HttpClient-UrlConnectionClient" + }, + "utils": { + "packageName": "AwsJavaSdk-Core-Utils" + }, + "utils-lite": { + "packageName": "AwsJavaSdk-Core-UtilsLite" + }, + "imds": { + "packageName": "AwsJavaSdk-Imds" + }, + "crt-core": { + "packageName": "AwsJavaSdk-Core-CrtCore" + }, + "checksums-spi": { + "packageName": "AwsJavaSdk-Core-ChecksumsSpi" + }, + "checksums": { + "packageName": "AwsJavaSdk-Core-Checksums" + }, + "identity-spi": { + "packageName": "AwsJavaSdk-Core-IdentitySpi" + }, + "http-auth-spi": { + "packageName": "AwsJavaSdk-Core-HttpAuthSpi" + }, + "http-auth": { + "packageName": "AwsJavaSdk-Core-HttpAuth" + }, + "http-auth-aws": { + "packageName": "AwsJavaSdk-Core-HttpAuthAws" + }, + "http-auth-aws-crt": { + "packageName": "AwsJavaSdk-Core-HttpAuthAwsCrt" + }, + "http-auth-aws-eventstream": { + "packageName": "AwsJavaSdk-Core-HttpAuthAwsEventStream" + }, + "retries-spi": { + "packageName": "AwsJavaSdk-Core-RetriesSpi" + }, + "retries": { + "packageName": "AwsJavaSdk-Core-Retries" + }, + "dynamodb": { + "packageName": "AwsJavaSdk-DynamoDb" + }, + "waf": { + "packageName": "AwsJavaSdk-Waf" + }, + "v2-migration": { + "packageName": "AwsJavaSdk-Tooling-V2Migration" + }, + "third-party-jackson-core": { + "packageName": "AwsJavaSdk-ThirdParty-JacksonCore", + "artifactType": "JAR", + "includes": [ + "target/aws-sdk-java-third-party-jackson-core-*.jar" + ], + "excludes": [ + "aws-sdk-java-third-party-jackson-core-*-javadoc.jar" + ] + }, + "third-party-jackson-dataformat-cbor": { + "packageName": "AwsJavaSdk-ThirdParty-JacksonDataformatCbor", + "artifactType": "JAR", + "includes": [ + "target/aws-sdk-java-third-party-jackson-dataformat-cbor-*.jar" + ], + "excludes": [ + "aws-sdk-java-third-party-jackson-dataformat-cbor-*-javadoc.jar" + ] + }, + "ruleset-testing-core": { + "packageName": "AwsJavaSdk-Test-RuleSetTestingCore" + }, + "archetype-app-quickstart": { + "skipImport": true + }, + "archetype-lambda": { + "skipImport": true + }, + "archetype-tools": { + "skipImport": true + }, + "archetypes": { + "skipImport": true + }, + "auth-tests": { + "skipImport": true + }, + "aws-sdk-java": { + "skipImport": true + }, + "aws-sdk-java-pom": { + "skipImport": true + }, + "bom": { + "skipImport": true + }, + "bom-internal": { + "skipImport": true + }, + "build-tools": { + "skipImport": true + }, + "bundle": { + "skipImport": true + }, + "bundle-sdk": { + "skipImport": true + }, + "bundle-logging-bridge": { + "skipImport": true + }, + "bundle-logging-bridge-binding-test": { + "skipImport": true + }, + "codegen-generated-classes-test": { + "skipImport": true + }, + "codegen-lite": { + "skipImport": true + }, + "codegen-lite-maven-plugin": { + "skipImport": true + }, + "codegen-maven-plugin": { + "skipImport": true + }, + "core": { + "skipImport": true + }, + "dynamodbdocument-v1": { + "skipImport": true + }, + "dynamodbmapper-v1": { + "skipImport": true + }, + "http-client-tests": { + "skipImport": true + }, + "http-clients": { + "skipImport": true + }, + "metric-publishers": { + "skipImport": true + }, + "module-path-tests": { + "skipImport": true + }, + "old-client-version-compatibility-test": { + "skipImport": true + }, + "protocol-tests": { + "skipImport": true + }, + "protocol-tests-core": { + "skipImport": true + }, + "protocols": { + "skipImport": true + }, + "region-testing": { + "skipImport": true + }, + "release-scripts": { + "skipImport": true + }, + "s3-benchmarks": { + "skipImport": true + }, + "http-client-benchmarks": { + "skipImport": true + }, + "sdk-benchmarks": { + "skipImport": true + }, + "sdk-standard-benchmarks": { + "skipImport": true + }, + "sdk-native-image-test": { + "skipImport": true + }, + "service-test-utils": { + "skipImport": true + }, + "services": { + "skipImport": true + }, + "services-custom": { + "skipImport": true + }, + "stability-tests": { + "skipImport": true + }, + "test-utils": { + "skipImport": true + }, + "tests-coverage-reporting": { + "skipImport": true + }, + "third-party": { + "skipImport": true + }, + "third-party-slf4j-api": { + "skipImport": true + }, + "v2-migration-tests": { + "skipImport": true + }, + "crt-unavailable-tests": { + "skipImport": true + }, + "bundle-shading-tests": { + "skipImport": true + }, + "architecture-tests": { + "skipImport": true + }, + "s3-tests": { + "skipImport": true + }, + "dynamodb-mapper": { + "packageName": "AwsJavaSdk-DynamoDb-Mapper" + } + }, + "dependencies": { + "com.amazonaws:aws-java-sdk-core": { + "packageName": "AWSJavaClientRuntime", + "packageVersion": "1.12.x" + }, + "com.amazonaws:aws-java-sdk-dynamodb": { + "packageName": "AWSDynamoDBJavaModel", + "packageVersion": "1.12.x" + }, + "com.amazonaws:aws-java-sdk-s3": { + "packageName": "AWSS3JavaClient", + "packageVersion": "1.12.x" + }, + "joda-time:joda-time": { + "packageName": "Joda-time", + "packageVersion": "2.x" + }, + "com.fasterxml.jackson.core:jackson-annotations": { + "packageName": "Jackson-annotations", + "packageVersion": "2.9.x" + }, + "com.fasterxml.jackson.core:jackson-core": { + "packageName": "Jackson-core", + "packageVersion": "2.9.x" + }, + "com.fasterxml.jackson.core:jackson-databind": { + "packageName": "Jackson-databind", + "packageVersion": "2.12.x" + }, + "com.fasterxml.jackson.datatype:jackson-datatype-jdk8": { + "packageName": "Jackson-datatype-jdk8", + "packageVersion": "2.12.x" + }, + "com.fasterxml.jackson.dataformat:jackson-dataformat-cbor": { + "packageName": "Jackson-dataformat-cbor", + "packageVersion": "2.9.x" + }, + "com.fasterxml.jackson.jr:jackson-jr-objects": { + "packageName": "Maven-com-fasterxml-jackson-jr_jackson-jr-objects", + "packageVersion": "2.11.x" + }, + "com.fasterxml.jackson.jr:jackson-jr-stree": { + "packageName": "Maven-com-fasterxml-jackson-jr_jackson-jr-stree", + "packageVersion": "2.11.x" + }, + "com.jayway.jsonpath:json-path": { + "packageName": "JMESPathJava", + "packageVersion": "1.0" + }, + "com.squareup:javapoet": { + "packageName": "Maven-com-squareup_javapoet", + "packageVersion": "1.13.x" + }, + "com.typesafe.netty:netty-reactive-streams": { + "packageName": "Netty4", + "packageVersion": "4.1" + }, + "com.typesafe.netty:netty-reactive-streams-http": { + "packageName": "Maven-com-typesafe-netty_netty-reactive-streams-http", + "packageVersion": "2.x" + }, + "commons-codec:commons-codec": { + "packageName": "JakartaCommons-codec", + "packageVersion": "1.x" + }, + "io.netty:netty-buffer": { + "packageName": "Netty4", + "packageVersion": "4.1" + }, + "io.netty:netty-codec": { + "packageName": "Netty4", + "packageVersion": "4.1" + }, + "io.netty:netty-codec-http": { + "packageName": "Netty4", + "packageVersion": "4.1" + }, + "io.netty:netty-codec-http2": { + "packageName": "Netty4", + "packageVersion": "4.1" + }, + "io.netty:netty-common": { + "packageName": "Netty4", + "packageVersion": "4.1" + }, + "io.netty:netty-handler": { + "packageName": "Netty4", + "packageVersion": "4.1" + }, + "io.netty:netty-resolver": { + "packageName": "Netty4", + "packageVersion": "4.1" + }, + "io.netty:netty-resolver-dns": { + "packageName": "Netty4", + "packageVersion": "4.1" + }, + "io.netty:netty-transport": { + "packageName": "Netty4", + "packageVersion": "4.1" + }, + "io.netty:netty-transport-classes-epoll": { + "packageName": "Netty4", + "packageVersion": "4.1" + }, + "io.netty:netty-transport-native-unix-common": { + "packageName": "Netty4", + "packageVersion": "4.1" + }, + "org.apache.httpcomponents:httpclient": { + "packageName": "Apache-HttpComponents-HttpClient", + "packageVersion": "4.5.x" + }, + "org.apache.httpcomponents.client5:httpclient5": { + "packageName": "Apache-HttpComponents-HttpClient5", + "packageVersion": "5.0.x" + }, + "org.apache.httpcomponents:httpcore": { + "packageName": "Apache-HttpComponents-HttpCore", + "packageVersion": "4.4.x" + }, + "org.apache.httpcomponents.core5:httpcore5": { + "packageName": "Apache-HttpComponents-HttpCore5", + "packageVersion": "5.0.x" + }, + "org.eclipse.jdt:org.eclipse.jdt.core": { + "packageName": "AwsJavaSdk-Codegen-EclipseJdtDependencies", + "packageVersion": "2.0" + }, + "org.eclipse.text:org.eclipse.text": { + "packageName": "AwsJavaSdk-Codegen-EclipseJdtDependencies", + "packageVersion": "2.0" + }, + "org.reactivestreams:reactive-streams": { + "packageName": "Maven-org-reactivestreams_reactive-streams", + "packageVersion": "1.x" + }, + "org.slf4j:slf4j-api": { + "packageName": "Slf4j", + "packageVersion": "1.7" + }, + "software.amazon.awssdk.crt:aws-crt": { + "packageName": "Aws-crt-java", + "packageVersion": "1.0.x" + }, + "software.amazon.eventstream:eventstream": { + "packageName": "AwsFlowJava", + "packageVersion": "1.0" + }, + "software.amazon.ion:ion-java": { + "packageName": "Maven-software-amazon-ion_ion-java", + "packageVersion": "1.x" + }, + "software.amazon:flow": { + "packageName": "AwsFlowJava", + "packageVersion": "1.0" + }, + "org.junit.jupiter:junit-jupiter": { + "packageName": "JUnit5", + "packageVersion": "5.x" + }, + "org.mockito:mockito-core": { + "packageName": "Mockito", + "packageVersion": "3.x" + }, + "io.reactivex.rxjava3:rxjava": { + "packageName": "RxJava", + "packageVersion": "3.x" + }, + "org.assertj:assertj-core": { + "packageName": "AssertJ", + "packageVersion": "3.22.x" + }, + "org.openrewrite:rewrite-java-8": { + "packageName": "Maven-org-openrewrite_rewrite-java-8", + "packageVersion": "8.x" + }, + "org.openrewrite:rewrite-java": { + "packageName": "Maven-org-openrewrite_rewrite-java", + "packageVersion": "8.x" + }, + "org.openrewrite.recipe:rewrite-java-dependencies": { + "packageName": "Maven-org-openrewrite-recipe_rewrite-java-dependencies", + "packageVersion": "1.x" } + }, + "test-dependencies": { + "s3": [ + "JUnit5-5.x", + "Aws-crt-java-1.0.x" + ] + } } diff --git a/aws-sdk-java/pom.xml b/aws-sdk-java/pom.xml index 737e3f2f2211..9bb8bc1bdf13 100644 --- a/aws-sdk-java/pom.xml +++ b/aws-sdk-java/pom.xml @@ -298,6 +298,11 @@ Amazon AutoScaling, etc). dynamodb-enhanced ${awsjavasdk.version} + + software.amazon.awssdk + dynamodb-mapper + ${awsjavasdk.version} + software.amazon.awssdk ec2 diff --git a/bom/pom.xml b/bom/pom.xml index 4c0fe2e7f565..89ee6b302daa 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -531,6 +531,10 @@ software.amazon.awssdk dynamodb-enhanced + + + software.amazon.awssdk + dynamodb-mapper ${awsjavasdk.version} diff --git a/build-tools/src/main/resources/software/amazon/awssdk/checkstyle-suppressions.xml b/build-tools/src/main/resources/software/amazon/awssdk/checkstyle-suppressions.xml index 4e81373b0be3..bcccae6e04a5 100644 --- a/build-tools/src/main/resources/software/amazon/awssdk/checkstyle-suppressions.xml +++ b/build-tools/src/main/resources/software/amazon/awssdk/checkstyle-suppressions.xml @@ -72,4 +72,8 @@ + + diff --git a/build-tools/src/main/resources/software/amazon/awssdk/spotbugs-suppressions.xml b/build-tools/src/main/resources/software/amazon/awssdk/spotbugs-suppressions.xml index 05606dc6d574..abd579363262 100644 --- a/build-tools/src/main/resources/software/amazon/awssdk/spotbugs-suppressions.xml +++ b/build-tools/src/main/resources/software/amazon/awssdk/spotbugs-suppressions.xml @@ -547,4 +547,11 @@ + + + + + + diff --git a/buildspecs/resources/generate-modules-to-skip.sh b/buildspecs/resources/generate-modules-to-skip.sh index 36ac5247b715..edbe08f48d65 100755 --- a/buildspecs/resources/generate-modules-to-skip.sh +++ b/buildspecs/resources/generate-modules-to-skip.sh @@ -3,7 +3,8 @@ # # Usage: # generate-modules-to-skip.sh --release Modules to skip for Maven Central release publishing. -# Skips all test/ modules except those in test-modules-publish-allowlist.txt. +# Skips all test/ modules except those in test-modules-publish-allowlist.txt, +# plus dynamodb-mapper. # generate-modules-to-skip.sh --docs Modules to skip for javadoc generation. # Skips all test/ modules + v2-migration. set -euo pipefail @@ -14,7 +15,7 @@ ALLOWLIST="$SCRIPT_DIR/test-modules-publish-allowlist.txt" if [[ "$1" == "--docs" ]]; then echo "$(ls "$REPO_ROOT/test/" | tr '\n' ',' | sed 's/,$//'),v2-migration" elif [[ "$1" == "--release" ]]; then - ls "$REPO_ROOT/test/" | grep -vxFf "$ALLOWLIST" | tr '\n' ',' | sed 's/,$//' + echo "$(ls "$REPO_ROOT/test/" | grep -vxFf "$ALLOWLIST" | tr '\n' ',' | sed 's/,$//'),dynamodb-mapper" else echo "Usage: $0 --release | --docs" >&2 exit 1 diff --git a/pom.xml b/pom.xml index 134c4f07b4b1..739c750d6a16 100644 --- a/pom.xml +++ b/pom.xml @@ -628,6 +628,7 @@ dynamodb-enhanced + dynamodb-mapper s3-transfer-manager iam-policy-builder s3-event-notifications diff --git a/services-custom/dynamodb-mapper/pom.xml b/services-custom/dynamodb-mapper/pom.xml new file mode 100644 index 000000000000..f71b4685c5ce --- /dev/null +++ b/services-custom/dynamodb-mapper/pom.xml @@ -0,0 +1,257 @@ + + + + + 4.0.0 + + software.amazon.awssdk + services-custom + 2.47.5-SNAPSHOT + + dynamodb-mapper + AWS Java SDK :: DynamoDB :: Mapper + DynamoDB Mapper v2 - a port of the v1 DynamoDBMapper for the AWS SDK for Java v2 + https://aws.amazon.com/sdkforjava + + + ${project.parent.version} + 1.8 + 1.12.797 + true + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + + software/amazon/awssdk/mapper/dynamodb/ConfigureS3LinksTest.java + software/amazon/awssdk/mapper/dynamodb/DynamoDBS3IntegrationTest.java + software/amazon/awssdk/mapper/dynamodb/DynamoDBS3IntegrationTestBase.java + software/amazon/awssdk/mapper/dynamodb/S3ClientCacheIntegrationTest.java + software/amazon/awssdk/mapper/dynamodb/S3LinkIDTest.java + software/amazon/awssdk/mapper/dynamodb/S3LinkTest.java + + software/amazon/awssdk/mapper/dynamodb/JsonIntegrationTest.java + software/amazon/awssdk/mapper/dynamodb/GenerateDeleteTableRequestTest.java + software/amazon/awssdk/mapper/dynamodb/mapper/GenerateCreateTableRequest2Test.java + software/amazon/awssdk/mapper/dynamodb/mapper/GenerateCreateTableRequestTest.java + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + software.amazon.awssdk.mapper.dynamodb + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy + test-compile + + copy-dependencies + + + test + so,dll,dylib + ${project.build.directory}/native-libs + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + -Dsqlite4java.library.path=${project.build.directory}/native-libs + + 0 + + + + + + + + + + software.amazon.awssdk + dynamodb + ${awsjavasdk.version} + + + software.amazon.awssdk + sdk-core + ${awsjavasdk.version} + + + software.amazon.awssdk + aws-core + ${awsjavasdk.version} + + + software.amazon.awssdk + http-client-spi + ${awsjavasdk.version} + + + software.amazon.awssdk + auth + ${awsjavasdk.version} + + + software.amazon.awssdk + regions + ${awsjavasdk.version} + + + software.amazon.awssdk + annotations + ${awsjavasdk.version} + + + software.amazon.awssdk + utils + ${awsjavasdk.version} + + + com.amazonaws + aws-java-sdk-dynamodb + ${sdk-v1.version} + + + com.amazonaws + aws-java-sdk-s3 + ${sdk-v1.version} + + + com.amazonaws + aws-java-sdk-core + ${sdk-v1.version} + + + joda-time + joda-time + 2.12.7 + + + + + junit + junit + test + + + org.mockito + mockito-core + test + + + org.apache.logging.log4j + log4j-api + test + + + org.apache.logging.log4j + log4j-core + test + + + org.apache.logging.log4j + log4j-slf4j-impl + test + + + org.hamcrest + hamcrest-all + test + + + org.junit.vintage + junit-vintage-engine + test + + + com.amazonaws + DynamoDBLocal + test + + + com.almworks.sqlite4java + sqlite4java + test + + + com.almworks.sqlite4java + sqlite4java-win32-x86 + dll + test + + + com.almworks.sqlite4java + sqlite4java-win32-x64 + dll + test + + + com.almworks.sqlite4java + libsqlite4java-osx + dylib + test + + + com.almworks.sqlite4java + libsqlite4java-linux-i386 + so + test + + + com.almworks.sqlite4java + libsqlite4java-linux-amd64 + so + test + + + diff --git a/services-custom/dynamodb-mapper/src/it/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBS3IntegrationTest.java b/services-custom/dynamodb-mapper/src/it/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBS3IntegrationTest.java new file mode 100644 index 000000000000..aab4a02846c4 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/it/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBS3IntegrationTest.java @@ -0,0 +1,98 @@ +/* + * 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.mapper.dynamodb; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.net.URL; +import java.util.UUID; + +import org.junit.Test; + +import com.amazonaws.internal.StaticCredentialsProvider; +import software.amazon.awssdk.mapper.dynamodb.pojos.S3LinksTestClass; +import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.PutObjectRequest; +import com.amazonaws.services.s3.model.Region; +import software.amazon.awssdk.mapper.dynamodb.test.util.RandomTempFile; + +public class DynamoDBS3IntegrationTest extends DynamoDBS3IntegrationTestBase { + + private final long OBJECT_SIZE = 123; + + @Test + public void testCredentialContext() throws Exception { + tryCreateItem(new DynamoDBMapper(dynamo, new StaticCredentialsProvider(credentials))); + } + + @Test + public void testManuallyFilledContext() throws Exception { + DynamoDBMapper mapper = new DynamoDBMapper(dynamo, new StaticCredentialsProvider(credentials)); + S3ClientCache s3cc = mapper.getS3ClientCache(); + s3cc.useClient(s3East); + s3cc.useClient(s3West); + tryCreateItem(mapper); + } + + public void tryCreateItem(DynamoDBMapper mapper) throws Exception { + String westKey = UUID.randomUUID().toString(); + String eastKey = UUID.randomUUID().toString(); + + S3LinksTestClass obj = new S3LinksTestClass(); + obj.setKey("" + ++startKey); + S3Link linkWest = mapper.createS3Link(Region.US_West_2, DynamoDBS3IntegrationTestBase.WEST_BUCKET, westKey); + obj.setS3LinkWest(linkWest); + mapper.save(obj); + obj = mapper.load(S3LinksTestClass.class, obj.getKey()); + + assertObjectDoesntExist(s3West, obj.getS3LinkWest().getBucketName(), westKey); + assertObjectDoesntExist(s3East, obj.getS3LinkWest().getBucketName(), eastKey); + + PutObjectRequest pubObjectReq = new PutObjectRequest(linkWest.getBucketName(), linkWest.getKey(), new RandomTempFile(westKey, OBJECT_SIZE)); + linkWest.getAmazonS3Client().putObject(pubObjectReq); + + assertObjectExists(s3West, obj.getS3LinkWest().getBucketName(), westKey); + + S3Link linkEast = mapper.createS3Link(Region.US_Standard, DynamoDBS3IntegrationTestBase.EAST_BUCKET, eastKey); + obj.setS3LinkEast(linkEast); + assertObjectDoesntExist(s3East, obj.getS3LinkEast().getBucketName(), eastKey); + pubObjectReq = new PutObjectRequest(linkEast.getBucketName(), linkEast.getKey(), new RandomTempFile(westKey, OBJECT_SIZE)); + linkEast.getAmazonS3Client().putObject(pubObjectReq); + mapper.save(obj); + + assertObjectExists(s3West, obj.getS3LinkWest().getBucketName(), westKey); + assertObjectExists(s3East, obj.getS3LinkEast().getBucketName(), eastKey); + + obj = mapper.load(S3LinksTestClass.class, obj.getKey()); + + assertEquals(westKey, obj.getS3LinkWest().getKey()); + assertEquals(eastKey, obj.getS3LinkEast().getKey()); + System.err.println(obj.getS3LinkWest().toJson()); + System.err.println(obj.getS3LinkEast().toJson()); + mapper.delete(obj); + + // Test the convenience methods on S3Link + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + linkEast.downloadTo(baos); + assertEquals(OBJECT_SIZE, baos.toByteArray().length); + + URL url = linkEast.getUrl(); + assertTrue(url.getHost().startsWith(linkEast.getBucketName())); + assertTrue(url.getPath().contains(linkEast.getKey())); + } +} diff --git a/services-custom/dynamodb-mapper/src/it/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBS3IntegrationTestBase.java b/services-custom/dynamodb-mapper/src/it/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBS3IntegrationTestBase.java new file mode 100644 index 000000000000..75a6cd71e84c --- /dev/null +++ b/services-custom/dynamodb-mapper/src/it/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBS3IntegrationTestBase.java @@ -0,0 +1,167 @@ +/* + * 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.mapper.dynamodb; + +import static org.junit.Assert.fail; + +import com.amazonaws.waiters.WaiterParameters; +import java.util.Iterator; + +import org.junit.AfterClass; +import org.junit.BeforeClass; + +import software.amazon.awssdk.mapper.dynamodb.test.util.DynamoDBIntegrationTestBase; +import com.amazonaws.regions.Region; +import com.amazonaws.regions.Regions; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.AmazonS3Exception; +import com.amazonaws.services.s3.model.GetObjectMetadataRequest; +import com.amazonaws.services.s3.model.ObjectListing; +import com.amazonaws.services.s3.model.S3ObjectSummary; + +public class DynamoDBS3IntegrationTestBase extends DynamoDBIntegrationTestBase { + public static final String WEST_BUCKET = "java-dynamo-s3-integ-test-west-" + System.currentTimeMillis(); + public static final String EAST_BUCKET = "java-dynamo-s3-integ-test-east-" + System.currentTimeMillis(); + + protected static AmazonS3Client s3East; + protected static AmazonS3Client s3West; + + @BeforeClass + public static void setUp() throws Exception { + DynamoDBIntegrationTestBase.setUp(); + s3East = new AmazonS3Client(credentials); + s3East.setRegion(Region.getRegion(Regions.US_EAST_1)); + s3West = new AmazonS3Client(credentials); + s3West.setRegion(Region.getRegion(Regions.US_WEST_2)); + + createBucket(s3East, EAST_BUCKET); + createBucket(s3West, WEST_BUCKET); + } + + @AfterClass + public static void tearDown() { + deleteBucketAndAllContents(s3East, EAST_BUCKET); + deleteBucketAndAllContents(s3West, WEST_BUCKET); + } + + /** + * Deletes all objects in the specified bucket, and then deletes the bucket. + * + * @param s3 + * The AmazonS3 client to use. + * @param bucketName + * The bucket to empty and delete. + */ + protected static void deleteBucketAndAllContents(AmazonS3 s3, String bucketName) { + ObjectListing objectListing = s3.listObjects(bucketName); + + while (true) { + for ( Iterator iterator = objectListing.getObjectSummaries().iterator(); iterator.hasNext(); ) { + S3ObjectSummary objectSummary = (S3ObjectSummary) iterator.next(); + s3.deleteObject(bucketName, objectSummary.getKey()); + } + + if (objectListing.isTruncated()) { + objectListing = s3.listNextBatchOfObjects(objectListing); + } else { + break; + } + }; + + s3.deleteBucket(bucketName); + } + + /** + * Asserts that the object stored in the specified bucket and key doesn't + * exist If it does exist, this method will fail the current test. + * + * @param s3 + * The AmazonS3 client to use. + * @param bucketName + * The name of the bucket containing the object to test. + * @param key + * The key under which the object is stored in the specified + * bucket. + */ + protected void assertObjectDoesntExist(AmazonS3 s3, String bucketName, String key) throws Exception { + long timeoutTime = System.currentTimeMillis() + 10000; + + while (true) { + try { + s3.getObjectMetadata(new GetObjectMetadataRequest(bucketName, key)); + Thread.sleep(1000); + if (System.currentTimeMillis() > timeoutTime) { + fail("object " + bucketName + "/" + key + " still exists"); + } + } catch (AmazonS3Exception ase) { + /* + * We expect a 404 indicating that the object version we requested + * doesn't exist. If we get anything other than that, then we want + * to let the exception keep going up the chain. + */ + if (ase.getStatusCode() != 404) throw ase; + return; // doesn't exist! + } + } + } + + /** + * Asserts that the object stored in the specified bucket and key exists. If + * it doesn't exist, this method will fail the current test. + * + * @param s3 + * The AmazonS3 client to use. + * @param bucketName + * The name of the bucket containing the object to test. + * @param key + * The key under which the object is stored in the specified + * bucket. + */ + protected void assertObjectExists(AmazonS3 s3, String bucketName, String key) throws Exception { + s3.waiters().objectExists().run(new WaiterParameters(new GetObjectMetadataRequest(bucketName, key))); + } + + /** + * Creates a bucket and waits for it to exist. + * + * @param s3 + * The AmazonS# client to use. + * @param bucketName + * The name of the bucket to create. + */ + protected static void createBucket(AmazonS3 s3, String bucketName) throws InterruptedException { + try { + s3.createBucket(bucketName); + } catch (AmazonS3Exception e) { + if (!e.getErrorCode().equals("BucketAlreadyOwnedByYou")) { + throw e; + } + } + + int poll = 0; + while ( !s3.doesBucketExist(bucketName) && poll++ < 60 ) { + Thread.sleep(1000); + } + if ( poll >= 60 * 5 ) { + maxPollTimeExceeded(); + } + } + + protected static void maxPollTimeExceeded() { + throw new RuntimeException("Max poll time exceeded"); + } +} diff --git a/services-custom/dynamodb-mapper/src/it/java/software/amazon/awssdk/mapper/dynamodb/JsonIntegrationTest.java b/services-custom/dynamodb-mapper/src/it/java/software/amazon/awssdk/mapper/dynamodb/JsonIntegrationTest.java new file mode 100644 index 000000000000..d8ceb1024c78 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/it/java/software/amazon/awssdk/mapper/dynamodb/JsonIntegrationTest.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.mapper.dynamodb; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import software.amazon.awssdk.mapper.dynamodb.pojos.TestDocClass; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.ConsistentReads; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.TableNameOverride; +import com.amazonaws.services.dynamodbv2.model.CreateTableRequest; +import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput; +import com.amazonaws.services.dynamodbv2.model.ResourceNotFoundException; +import com.amazonaws.services.dynamodbv2.model.TableStatus; +import software.amazon.awssdk.mapper.dynamodb.test.AWSTestBase; + +import static software.amazon.awssdk.mapper.dynamodb.pojos.TestDocClass.ChildClass; + +public class JsonIntegrationTest extends AWSTestBase { + + private static final String TABLE_NAME = "test-table-" + + UUID.randomUUID().toString(); + + private static AmazonDynamoDBClient client; + private static DynamoDBMapper mapper; + + @BeforeClass + public static void setup() throws Exception { + setUpCredentials(); + client = new AmazonDynamoDBClient(credentials); + + mapper = new DynamoDBMapper( + client, + new DynamoDBMapperConfig.Builder() + .withConversionSchema(ConversionSchemas.V2) + .withTableNameOverride(TableNameOverride + .withTableNameReplacement(TABLE_NAME)) + .withConsistentReads(ConsistentReads.CONSISTENT) + .build()); + + CreateTableRequest request = mapper + .generateCreateTableRequest(TestDocClass.class) + .withProvisionedThroughput(new ProvisionedThroughput(1L, 1L)); + + client.createTable(request); + + Thread.sleep(10000); + + while (true) { + String status = client.describeTable(TABLE_NAME) + .getTable() + .getTableStatus(); + + if (status.equals(TableStatus.ACTIVE.toString())) { + break; + } else if (!status.equals(TableStatus.CREATING.toString())) { + throw new RuntimeException("Table creation failed"); + } + + Thread.sleep(2000); + } + } + + @AfterClass + public static void cleanup() { + if (client == null) { + return; + } + + try { + client.deleteTable(TABLE_NAME); + } catch (ResourceNotFoundException e) { + } + } + + @Test + public void testIt() { + final ChildClass child1 = new ChildClass(); + child1.setBool(true); + + final ChildClass child2 = new ChildClass(); + child2.setBool(true); + + final ChildClass parent = new ChildClass(); + parent.setFirstChild(child1); + parent.setOtherChildren(Arrays.asList(child1, child2)); + parent.setNamedChildren(new HashMap() {{ + put("one", child1); + put("two", child2); + }}); + + TestDocClass test = new TestDocClass(); + test.setId("test"); + test.setListOfMaps(Arrays.>asList( + new HashMap() {{ + put("parent", parent); + }}, + new HashMap() {{ + put("parent", parent); + }}, + null + )); + test.setMapOfLists(new HashMap>() {{ + put("parent", Arrays.asList(child1, child2)); + put("child2", Collections.emptyList()); + put("child1", null); + }}); + + mapper.save(test); + + TestDocClass result = mapper.load(TestDocClass.class, "test"); + + Assert.assertEquals(test, result); + } + +} diff --git a/services-custom/dynamodb-mapper/src/it/java/software/amazon/awssdk/mapper/dynamodb/S3ClientCacheIntegrationTest.java b/services-custom/dynamodb-mapper/src/it/java/software/amazon/awssdk/mapper/dynamodb/S3ClientCacheIntegrationTest.java new file mode 100644 index 000000000000..9d6e6647d20e --- /dev/null +++ b/services-custom/dynamodb-mapper/src/it/java/software/amazon/awssdk/mapper/dynamodb/S3ClientCacheIntegrationTest.java @@ -0,0 +1,126 @@ +/* + * 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.mapper.dynamodb; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import software.amazon.awssdk.mapper.dynamodb.test.AWSTestBase; +import org.junit.Before; +import org.junit.Test; + +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.Region; +import com.amazonaws.services.s3.transfer.TransferManager; + +public class S3ClientCacheIntegrationTest extends AWSTestBase { + private AWSCredentials credentials; + + @Before + public void setUp() { + credentials = new BasicAWSCredentials("mock", "mock"); + } + + @Test + public void testClientReuse() { + S3ClientCache s3cc = new S3ClientCache(credentials); + + TransferManager tmEast = s3cc.getTransferManager(Region.US_Standard); + AmazonS3 s3East = s3cc.getClient(Region.US_Standard); + + assertNotNull(tmEast); + assertNotNull(s3East); + assertSame(s3East, tmEast.getAmazonS3Client()); + + assertSame(s3East, s3cc.getClient(Region.US_Standard)); + assertSame(tmEast, s3cc.getTransferManager(Region.US_Standard)); + } + + @Test + public void testUserProvidedClients() { + S3ClientCache s3cc = new S3ClientCache(credentials); + AmazonS3Client s3East1 = new AmazonS3Client(credentials); + s3East1.setRegion(Region.US_Standard.toAWSRegion()); + AmazonS3Client s3West1 = new AmazonS3Client(credentials); + s3West1.setRegion(Region.US_West.toAWSRegion()); + AmazonS3Client s3West2 = new AmazonS3Client(credentials); + s3West2.setRegion(Region.US_West_2.toAWSRegion()); + + s3cc.useClient(s3East1); + s3cc.useClient(s3West1); + s3cc.useClient(s3West2); + + TransferManager tmEast1 = s3cc.getTransferManager(Region.US_Standard); + TransferManager tmWest1 = s3cc.getTransferManager(Region.US_West); + TransferManager tmWest2 = s3cc.getTransferManager(Region.US_West_2); + + assertNotSame(tmEast1, tmWest1); + assertNotSame(tmEast1, tmWest2); + assertNotSame(tmWest1, tmWest2); + + assertSame(s3cc.getClient(Region.US_Standard), tmEast1.getAmazonS3Client()); + assertSame(s3cc.getClient(Region.US_West), tmWest1.getAmazonS3Client()); + assertSame(s3cc.getClient(Region.US_West_2), tmWest2.getAmazonS3Client()); + } + + @Test + public void testS3ClientCacheWithRegionString() { + S3ClientCache s3cc = new S3ClientCache(credentials); + AmazonS3 s3 = AmazonS3ClientBuilder.standard().withRegion("us-east-2").withCredentials(new DefaultAWSCredentialsProviderChain()).build(); + s3cc.useClient(s3); + + TransferManager tm = s3cc.getTransferManager("us-east-2"); + + assertSame(s3cc.getClient("us-east-2"), s3); + assertSame(s3cc.getTransferManager("us-east-2"), tm); + } + + @Test + public void testReplaceClient() { + S3ClientCache s3cc = new S3ClientCache(credentials); + + TransferManager tmEast1 = s3cc.getTransferManager(Region.US_Standard); + assertNotNull(tmEast1); + + AmazonS3Client newS3East = new AmazonS3Client(credentials); + newS3East.setRegion(Region.US_Standard.toAWSRegion()); + s3cc.useClient(newS3East); // should remove old TM + + TransferManager tmEast2 = s3cc.getTransferManager(Region.US_Standard); + assertNotNull(tmEast2); + assertNotSame(tmEast2, tmEast1); + assertNotSame(tmEast2.getAmazonS3Client(), tmEast1.getAmazonS3Client()); + } + + @Test + public void testNonExistantRegion() { + S3ClientCache s3cc = new S3ClientCache(credentials); + AmazonS3Client notAnAWSEndpoint = new AmazonS3Client(credentials); + notAnAWSEndpoint.setEndpoint("s3.mordor.amazonaws.com"); + s3cc.useClient(notAnAWSEndpoint); + assertEquals(notAnAWSEndpoint, s3cc.getClient("mordor")); + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/AbstractDynamoDBMapper.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/AbstractDynamoDBMapper.java new file mode 100644 index 000000000000..b5069e06a4d2 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/AbstractDynamoDBMapper.java @@ -0,0 +1,358 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper.FailedBatch; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import com.amazonaws.services.dynamodbv2.model.CreateTableRequest; +import com.amazonaws.services.dynamodbv2.model.DeleteTableRequest; +import com.amazonaws.services.s3.model.Region; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * Abstract implementation of {@code IDynamoDBMapper}. Convenient method forms pass through to the + * corresponding overload that takes a request object, which throws an + * {@code UnsupportedOperationException}. + */ +@SdkPublicApi +public class AbstractDynamoDBMapper implements IDynamoDBMapper { + + private final DynamoDBMapperConfig config; + + protected AbstractDynamoDBMapper(final DynamoDBMapperConfig defaults) { + this.config = DynamoDBMapperConfig.DEFAULT.merge(defaults); + } + + protected AbstractDynamoDBMapper() { + this(DynamoDBMapperConfig.DEFAULT); + } + + protected final String getTableName(Class clazz, Object object, DynamoDBMapperConfig config) { + if (config.getObjectTableNameResolver() != null && object != null) { + return config.getObjectTableNameResolver().getTableName(object, config); + } + return getTableName(clazz, config); + } + + protected final String getTableName(Class clazz, DynamoDBMapperConfig config) { + if (config.getTableNameResolver() == null) { + return DynamoDBMapperConfig.DefaultTableNameResolver.INSTANCE.getTableName(clazz, config); + } + return config.getTableNameResolver().getTableName(clazz, config); + } + + protected final DynamoDBMapperConfig mergeConfig(DynamoDBMapperConfig overrides) { + return this.config.merge(overrides); + } + + @Override + public DynamoDBMapperTableModel getTableModel(Class clazz) { + return getTableModel(clazz, config); + } + + @Override + public DynamoDBMapperTableModel getTableModel(Class clazz, DynamoDBMapperConfig config) { + throw new UnsupportedOperationException("operation not supported in " + getClass()); + } + + @Override + public T load(Class clazz, Object hashKey, DynamoDBMapperConfig config) { + return load(clazz, hashKey, (Object)null, config); + } + + @Override + public T load(Class clazz, Object hashKey) { + return load(clazz, hashKey, (Object)null, config); + } + + @Override + public T load(Class clazz, Object hashKey, Object rangeKey) { + return load(clazz, hashKey, rangeKey, config); + } + + @Override + public T load(Class clazz, Object hashKey, Object rangeKey, DynamoDBMapperConfig config) { + throw new UnsupportedOperationException("operation not supported in " + getClass()); + } + + @Override + public T load(T keyObject) { + return load(keyObject, config); + } + + @Override + public T load(T keyObject, DynamoDBMapperConfig config) { + throw new UnsupportedOperationException("operation not supported in " + getClass()); + } + + @Override + public T marshallIntoObject(Class clazz, Map itemAttributes) { + return marshallIntoObject(clazz, itemAttributes, config); + } + + public T marshallIntoObject(Class clazz, Map itemAttributes, DynamoDBMapperConfig config) { + throw new UnsupportedOperationException("operation not supported in " + getClass()); + } + + @Override + public List marshallIntoObjects(Class clazz, List> itemAttributes) { + return marshallIntoObjects(clazz, itemAttributes, config); + } + + public List marshallIntoObjects(Class clazz, List> itemAttributes, DynamoDBMapperConfig config) { + throw new UnsupportedOperationException("operation not supported in " + getClass()); + } + + @Override + public void save(T object) { + save(object, (DynamoDBSaveExpression)null, config); + } + + @Override + public void save(T object, DynamoDBSaveExpression saveExpression) { + save(object, saveExpression, config); + } + + @Override + public void save(T object, DynamoDBMapperConfig config) { + save(object, (DynamoDBSaveExpression)null, config); + } + + @Override + public void save(T object, DynamoDBSaveExpression saveExpression, DynamoDBMapperConfig config) { + throw new UnsupportedOperationException("operation not supported in " + getClass()); + } + + @Override + public void delete(Object object) { + delete(object, (DynamoDBDeleteExpression)null, config); + } + + @Override + public void delete(Object object, DynamoDBDeleteExpression deleteExpression) { + delete(object, deleteExpression, config); + } + + @Override + public void delete(Object object, DynamoDBMapperConfig config) { + delete(object, (DynamoDBDeleteExpression)null, config); + } + + @Override + public void delete(T object, DynamoDBDeleteExpression deleteExpression, DynamoDBMapperConfig config) { + throw new UnsupportedOperationException("operation not supported in " + getClass()); + } + + @Override + public void transactionWrite(TransactionWriteRequest transactionWriteRequest) { + transactionWrite(transactionWriteRequest, (DynamoDBMapperConfig) null); + } + + @Override + public void transactionWrite(TransactionWriteRequest transactionWriteRequest, DynamoDBMapperConfig config) { + throw new UnsupportedOperationException("operation not supported in " + getClass()); + } + + @Override + public List transactionLoad(TransactionLoadRequest transactionLoadRequest) { + return transactionLoad(transactionLoadRequest, (DynamoDBMapperConfig) null); + } + + @Override + public List transactionLoad(TransactionLoadRequest transactionLoadRequest, DynamoDBMapperConfig config) { + throw new UnsupportedOperationException("operation not supported in " + getClass()); + } + + @Override + public List batchDelete(Iterable objectsToDelete) { + return batchWrite(Collections.emptyList(), objectsToDelete, config); + } + + @Override + public List batchDelete(Object... objectsToDelete) { + return batchWrite(Collections.emptyList(), Arrays.asList(objectsToDelete), config); + } + + @Override + public List batchSave(Iterable objectsToSave) { + return batchWrite(objectsToSave, Collections.emptyList(), config); + } + + @Override + public List batchSave(Object... objectsToSave) { + return batchWrite(Arrays.asList(objectsToSave), Collections.emptyList(), config); + } + + @Override + public List batchWrite(Iterable objectsToWrite, + Iterable objectsToDelete) { + return batchWrite(objectsToWrite, objectsToDelete, config); + } + + @Override + public List batchWrite(Iterable objectsToWrite, + Iterable objectsToDelete, + DynamoDBMapperConfig config) { + throw new UnsupportedOperationException("operation not supported in " + getClass()); + } + + @Override + public Map> batchLoad(Iterable itemsToGet) { + return batchLoad(itemsToGet, config); + } + + @Override + public Map> batchLoad(Iterable itemsToGet, DynamoDBMapperConfig config) { + throw new UnsupportedOperationException("operation not supported in " + getClass()); + } + + @Override + public Map> batchLoad(Map, List> itemsToGet) { + return batchLoad(itemsToGet, config); + } + + @Override + public Map> batchLoad(Map, List> itemsToGet, DynamoDBMapperConfig config) { + throw new UnsupportedOperationException("operation not supported in " + getClass()); + } + + @Override + public PaginatedScanList scan(Class clazz, DynamoDBScanExpression scanExpression) { + return scan(clazz, scanExpression, config); + } + + @Override + public PaginatedScanList scan(Class clazz, + DynamoDBScanExpression scanExpression, + DynamoDBMapperConfig config) { + throw new UnsupportedOperationException("operation not supported in " + getClass()); + } + + @Override + public PaginatedParallelScanList parallelScan(Class clazz, + DynamoDBScanExpression scanExpression, + int totalSegments) { + return parallelScan(clazz, scanExpression, totalSegments, config); + } + + @Override + public PaginatedParallelScanList parallelScan(Class clazz, + DynamoDBScanExpression scanExpression, + int totalSegments, + DynamoDBMapperConfig config) { + throw new UnsupportedOperationException("operation not supported in " + getClass()); + } + + @Override + public ScanResultPage scanPage(Class clazz, DynamoDBScanExpression scanExpression) { + return scanPage(clazz, scanExpression, config); + } + + @Override + public ScanResultPage scanPage(Class clazz, + DynamoDBScanExpression scanExpression, + DynamoDBMapperConfig config) { + throw new UnsupportedOperationException("operation not supported in " + getClass()); + } + + @Override + public int count(Class clazz, DynamoDBScanExpression scanExpression) { + return count(clazz, scanExpression, config); + } + + @Override + public int count(Class clazz, DynamoDBScanExpression scanExpression, DynamoDBMapperConfig config) { + throw new UnsupportedOperationException("operation not supported in " + getClass()); + } + + @Override + public PaginatedQueryList query(Class clazz, DynamoDBQueryExpression queryExpression) { + return query(clazz, queryExpression, config); + } + + @Override + public PaginatedQueryList query(Class clazz, + DynamoDBQueryExpression queryExpression, + DynamoDBMapperConfig config) { + throw new UnsupportedOperationException("operation not supported in " + getClass()); + } + + @Override + public QueryResultPage queryPage(Class clazz, DynamoDBQueryExpression queryExpression) { + return queryPage(clazz, queryExpression, config); + } + + @Override + public QueryResultPage queryPage(Class clazz, + DynamoDBQueryExpression queryExpression, + DynamoDBMapperConfig config) { + throw new UnsupportedOperationException("operation not supported in " + getClass()); + } + + @Override + public int count(Class clazz, DynamoDBQueryExpression queryExpression) { + return count(clazz, queryExpression, config); + } + + @Override + public int count(Class clazz, DynamoDBQueryExpression queryExpression, DynamoDBMapperConfig config) { + throw new UnsupportedOperationException("operation not supported in " + getClass()); + } + + @Override + public S3ClientCache getS3ClientCache() { + throw new UnsupportedOperationException("operation not supported in " + getClass()); + } + + @Override + public S3Link createS3Link(String bucketName, String key) { + return createS3Link((Region)null, bucketName, key); + } + + @Override + public S3Link createS3Link(Region s3region, String bucketName, String key) { + throw new UnsupportedOperationException("operation not supported in " + getClass()); + } + + @Override + public S3Link createS3Link(String s3region, String bucketName, String key) { + throw new UnsupportedOperationException("operation not supported in " + getClass()); + } + + @Override + public CreateTableRequest generateCreateTableRequest(Class clazz) { + return generateCreateTableRequest(clazz, config); + } + + public CreateTableRequest generateCreateTableRequest(Class clazz, DynamoDBMapperConfig config) { + throw new UnsupportedOperationException("operation not supported in " + getClass()); + } + + @Override + public DeleteTableRequest generateDeleteTableRequest(Class clazz) { + return generateDeleteTableRequest(clazz, config); + } + + public DeleteTableRequest generateDeleteTableRequest(Class clazz, DynamoDBMapperConfig config) { + throw new UnsupportedOperationException("operation not supported in " + getClass()); + } + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/AbstractEnumMarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/AbstractEnumMarshaller.java new file mode 100644 index 000000000000..182250068d81 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/AbstractEnumMarshaller.java @@ -0,0 +1,62 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.annotations.SdkPublicApi; + +/** + * Generic marshaller for enumerations. + * + * Please note, there are some risks in distributed systems when using + * enumerations as attributes instead of simply using a String. + * When adding new values to the enumeration, the enum only changes must + * be deployed before the enumeration value can be persisted. This will + * ensure that all systems have the correct code to map it from the item + * record in DynamoDB to your objects. + * + * @see DynamoDBMarshaller + * + * @deprecated Replaced by {@link DynamoDBTypeConvertedEnum} + */ +@SdkPublicApi +@Deprecated +public abstract class AbstractEnumMarshaller> implements DynamoDBMarshaller { + + /** + * {@inheritDoc} + */ + @Override + public String marshall(final T obj) { + try { + return obj.name(); + } catch (final Exception e) { + throw MapperExceptions.failure(e, "Unable to marshall the enum " + obj); + } + } + + /** + * {@inheritDoc} + */ + @Override + public T unmarshall(final Class clazz, final String obj) { + try { + return Enum.valueOf(clazz, obj); + } catch (final Exception e) { + throw MapperExceptions.failure(e, "Unable to unmarshall the enum value " + obj); + } + } + +} \ No newline at end of file diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/ArgumentMarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/ArgumentMarshaller.java new file mode 100644 index 000000000000..138ecac135d4 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/ArgumentMarshaller.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.mapper.dynamodb; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * Interface to make it possible to cache the expensive type determination + * behavior. + */ +@SdkPublicApi +public interface ArgumentMarshaller { + + /** + * Marshalls the object given into an AttributeValue. + */ + AttributeValue marshall(Object obj); + + interface BooleanAttributeMarshaller extends ArgumentMarshaller {} + interface StringAttributeMarshaller extends ArgumentMarshaller {} + interface NumberAttributeMarshaller extends ArgumentMarshaller {} + interface BinaryAttributeMarshaller extends ArgumentMarshaller {} + interface StringSetAttributeMarshaller extends ArgumentMarshaller {} + interface NumberSetAttributeMarshaller extends ArgumentMarshaller {} + interface BinarySetAttributeMarshaller extends ArgumentMarshaller {} + interface ListAttributeMarshaller extends ArgumentMarshaller {} + interface MapAttributeMarshaller extends ArgumentMarshaller {} +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/ArgumentUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/ArgumentUnmarshaller.java new file mode 100644 index 000000000000..6dac8660aed9 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/ArgumentUnmarshaller.java @@ -0,0 +1,44 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import java.lang.reflect.Method; +import java.text.ParseException; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * Unmarshaller interface to make it possible to cache the expensive + * type-determination behavior necessary when turning a service result back + * into an object. + */ +@SdkPublicApi +public interface ArgumentUnmarshaller { + + /** + * Asserts that the value given can be processed using the setter given. + */ + void typeCheck(AttributeValue value, Method setter); + + /** + * Unmarshalls the {@link AttributeValue} given into an instance of the + * appropriate type, as determined by {@link DynamoDBMapper} + * + * @throws ParseException when unable to parse a date string + */ + Object unmarshall(AttributeValue value) throws ParseException; +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/AttributeTransformer.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/AttributeTransformer.java new file mode 100644 index 000000000000..767c44a063a5 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/AttributeTransformer.java @@ -0,0 +1,117 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import java.util.Map; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * A hook allowing a custom transform/untransform of the raw attribute + * values immediately before writing them into DynamoDB and immediately + * after reading them out of DynamoDB, but with extra context about + * the model class not available at the raw AmazonDynamoDB level. + *

+ * This interface contains both a {@code transform} method and a corresponding + * {@code untransform} method. These methods SHOULD be inverses, such that + * untransform(transform(value)) == value. + */ +@SdkPublicApi +public interface AttributeTransformer { + /** + * Parameters for the {@code transform} and {@code untransform} methods, + * so we don't have to break the interface in order to add additional + * parameters. + *

+ * Consuming code should NOT implement this interface. + */ + interface Parameters { + /** + * Returns the raw attribute values to be transformed or untransformed. + * The returned map is not modifiable. + * + * @return the raw attribute values to transform or untransform + */ + Map getAttributeValues(); + + /** + * Returns true if this transformation is being called as part of a + * partial update operation. If true, the attributes returned by + * {@link #getAttributeValues()} do not represent the entire new + * item, but only a snapshot of the attributes which are getting + * new values. + *

+ * Implementations which do not support transforming a partial + * view of an item (for example, because they need to calculate a + * signature based on all of the item's attributes that won't be valid + * if only a subset of the attributes are taken into consideration) + * should check this flag and throw an exception rather than than + * corrupting the data in DynamoDB. + *

+ * This method always returns {@code false} for instances passed to + * {@link AttributeTransformer#untransform(Parameters)}. + * + * @return true if this operation is a partial update, false otherwise + */ + boolean isPartialUpdate(); + + /** + * @return the type of the model class we're transforming to or from + */ + Class getModelClass(); + + /** + * @return the mapper config for this operation + */ + DynamoDBMapperConfig getMapperConfig(); + + /** + * @return the name of the DynamoDB table the attributes were read + * from or will be written to + */ + String getTableName(); + + /** + * @return the name of the hash key for the table + */ + String getHashKeyName(); + + /** + * @return the name of the range key for the table, if it has one, + * otherwise {@code null} + */ + String getRangeKeyName(); + } + + /** + * Transforms the input set of attribute values derived from the model + * object before writing them into DynamoDB. + * + * @param parameters transformation parameters + * @return the transformed attribute value map + */ + Map transform(Parameters parameters); + + /** + * Untransform the input set of attribute values read from DynamoDB before + * creating a model object from them. + * + * @param parameters transformation parameters + * @return the untransformed attribute value map + */ + Map untransform(Parameters parameters); +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/AttributeTransformerChain.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/AttributeTransformerChain.java new file mode 100644 index 000000000000..872f82cc1205 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/AttributeTransformerChain.java @@ -0,0 +1,166 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * A virtual {@code AttributeTransformer} that transforms and untransforms + * attributes by running them through a cascading series of child + * {@code AttributeTransformer} instances. + */ +@SdkPublicApi +public final class AttributeTransformerChain implements AttributeTransformer { + + private final List transformers; + + /** + * Creates a new transformer chain from the given array of transformers. + * When transforming attributes, these transformers are invoked from first + * to last; when untransforming they are invoked in the opposite order. + * + * @param transformers the chain of transformers. + */ + public AttributeTransformerChain( + final AttributeTransformer... transformers) { + + this(Arrays.asList(transformers)); + } + + /** + * Creates a new transformer chain from the given list of transformers. + * When transforming attributes, these transformers are invoked from first + * to last; when untransforming they are invoked in the opposite order. + * + * @param transformers the chain of transformers. + */ + public AttributeTransformerChain( + final List transformers) { + + this.transformers = Collections.unmodifiableList( + new ArrayList(transformers)); + } + + /** + * @return the transformers in this chain + */ + public List getTransformers() { + return transformers; + } + + @Override + public Map transform( + final Parameters parameters) { + + ProxyParameters proxy = new ProxyParameters(parameters); + + for (int i = 0; i < transformers.size(); ++i) { + proxy.setAttributeValues(transformers.get(i).transform(proxy)); + } + + return proxy.getAttributeValues(); + } + + @Override + public Map untransform( + final Parameters parameters) { + + ProxyParameters proxy = new ProxyParameters(parameters); + + for (int i = transformers.size() - 1; i >= 0; --i) { + proxy.setAttributeValues(transformers.get(i).untransform(proxy)); + } + + return proxy.getAttributeValues(); + } + + @Override + public String toString() { + return transformers.toString(); + } + + /** + * A {@code Parameters} proxy that intercepts calls to + * {@code getAttributeValues} and overrides the return value. + */ + private static class ProxyParameters implements Parameters { + + private final Parameters delegate; + private Map values; + + /** + * Create a new proxy wrapping the given {@code Parameters} object. + * + * @param delegate the parameters object to wrap + */ + public ProxyParameters(final Parameters delegate) { + this.delegate = delegate; + this.values = delegate.getAttributeValues(); + } + + @Override + public Map getAttributeValues() { + return values; + } + + /** + * Changes the attribute values for this instance. + * + * @param values the new values + */ + public void setAttributeValues( + final Map values) { + this.values = Collections.unmodifiableMap(values); + } + + @Override + public boolean isPartialUpdate() { + return delegate.isPartialUpdate(); + } + + @Override + public Class getModelClass() { + return delegate.getModelClass(); + } + + @Override + public DynamoDBMapperConfig getMapperConfig() { + return delegate.getMapperConfig(); + } + + @Override + public String getTableName() { + return delegate.getTableName(); + } + + @Override + public String getHashKeyName() { + return delegate.getHashKeyName(); + } + + @Override + public String getRangeKeyName() { + return delegate.getRangeKeyName(); + } + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/BatchLoadContext.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/BatchLoadContext.java new file mode 100644 index 000000000000..cc2ee62c443b --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/BatchLoadContext.java @@ -0,0 +1,101 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import java.util.Objects; +import software.amazon.awssdk.services.dynamodb.model.BatchGetItemRequest; +import software.amazon.awssdk.services.dynamodb.model.BatchGetItemResponse; + + +/** + * Container object that has information about the batch load request made to DynamoDB. + * + * @author avinam + */ +@SdkPublicApi +public final class BatchLoadContext { + /** + * The BatchGetItemResponse returned by the DynamoDB client. + */ + private BatchGetItemResponse batchGetItemResult; + /** + * The BatchGetItemRequest. + */ + private BatchGetItemRequest batchGetItemRequest; + /** + * The number of times the request has been retried. + */ + private int retriesAttempted; + + /** + * Instantiates a new BatchLoadContext. + * @param batchGetItemRequest see {@link BatchGetItemRequest}. + * */ + public BatchLoadContext(BatchGetItemRequest batchGetItemRequest) { + this.batchGetItemRequest = Objects.requireNonNull(batchGetItemRequest, "batchGetItemRequest"); + this.batchGetItemResult = null; + this.retriesAttempted = 0; + } + + /** + * @return the BatchGetItemResponse + */ + public BatchGetItemResponse getBatchGetItemResult() { + return batchGetItemResult; + } + + /** + * @return the BatchGetItemResponse + */ + public void setBatchGetItemResult(BatchGetItemResponse batchGetItemResult) { + this.batchGetItemResult = batchGetItemResult; + } + + + /** + * @return the BatchGetItemRequest. + */ + public BatchGetItemRequest getBatchGetItemRequest() { + return batchGetItemRequest; + } + + /** + * Updates the BatchGetItemRequest for the next retry attempt. + */ + public void setBatchGetItemRequest(BatchGetItemRequest batchGetItemRequest) { + this.batchGetItemRequest = batchGetItemRequest; + } + + /** + * Gets the retriesAttempted. + * + * @return the retriesAttempted + */ + public int getRetriesAttempted() { + return retriesAttempted; + } + + /** + * Sets retriesAttempted. + * + * @param retriesAttempted the number of retries attempted + */ + public void setRetriesAttempted(int retriesAttempted) { + this.retriesAttempted = retriesAttempted; + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/ConversionSchema.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/ConversionSchema.java new file mode 100644 index 000000000000..0de861a1ed0c --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/ConversionSchema.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.mapper.dynamodb; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import java.util.HashMap; +import java.util.Map; + +/** + * A strategy for mapping between Java types and DynamoDB types. Serves as a + * factory for {@code ItemConverter} instances that implement this mapping. + * Standard implementations are available in the {@link ConversionSchemas} + * class. + */ +@SdkPublicApi +public interface ConversionSchema { + + /** + * Dependency injection for the {@code ItemConverter}s that this + * {@code ConversionSchema} generates. + */ + static class Dependencies { + + private final Map, Object> values; + + public Dependencies() { + values = new HashMap, Object>(); + } + + @SuppressWarnings("unchecked") + public T get(Class clazz) { + return (T) values.get(clazz); + } + + public Dependencies with(Class clazz, T value) { + values.put(clazz, value); + return this; + } + + @Override + public String toString() { + return values.toString(); + } + } + + /** + * Creates an {@code ItemConverter}, injecting dependencies from the + * {@code DynamoDBMapper} that needs it. + * + * @param dependencies the dependencies to inject + * @return a new ItemConverter + */ + ItemConverter getConverter(Dependencies dependencies); +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/ConversionSchemas.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/ConversionSchemas.java new file mode 100644 index 000000000000..a008dc186201 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/ConversionSchemas.java @@ -0,0 +1,1493 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.mapper.dynamodb.ArgumentMarshaller.BinaryAttributeMarshaller; +import software.amazon.awssdk.mapper.dynamodb.ArgumentMarshaller.BinarySetAttributeMarshaller; +import software.amazon.awssdk.mapper.dynamodb.ArgumentMarshaller.BooleanAttributeMarshaller; +import software.amazon.awssdk.mapper.dynamodb.ArgumentMarshaller.ListAttributeMarshaller; +import software.amazon.awssdk.mapper.dynamodb.ArgumentMarshaller.MapAttributeMarshaller; +import software.amazon.awssdk.mapper.dynamodb.ArgumentMarshaller.NumberAttributeMarshaller; +import software.amazon.awssdk.mapper.dynamodb.ArgumentMarshaller.NumberSetAttributeMarshaller; +import software.amazon.awssdk.mapper.dynamodb.ArgumentMarshaller.StringAttributeMarshaller; +import software.amazon.awssdk.mapper.dynamodb.ArgumentMarshaller.StringSetAttributeMarshaller; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperFieldModel.DynamoDBAttributeType; +import software.amazon.awssdk.mapper.dynamodb.StandardBeanProperties.Bean; +import software.amazon.awssdk.mapper.dynamodb.StandardModelFactories.Rule; +import software.amazon.awssdk.mapper.dynamodb.StandardModelFactories.RuleFactory; +import software.amazon.awssdk.mapper.dynamodb.internal.marshallers.BooleanSetToNumberSetMarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.marshallers.BooleanToBooleanMarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.marshallers.BooleanToNumberMarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.marshallers.ByteArraySetToBinarySetMarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.marshallers.ByteArrayToBinaryMarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.marshallers.ByteBufferSetToBinarySetMarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.marshallers.ByteBufferToBinaryMarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.marshallers.CalendarSetToStringSetMarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.marshallers.CalendarToStringMarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.marshallers.CollectionToListMarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.marshallers.CustomMarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.marshallers.DateSetToStringSetMarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.marshallers.DateToStringMarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.marshallers.MapToMapMarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.marshallers.NumberSetToNumberSetMarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.marshallers.NumberToNumberMarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.marshallers.ObjectSetToStringSetMarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.marshallers.ObjectToMapMarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.marshallers.ObjectToStringMarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.marshallers.S3LinkToStringMarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.marshallers.StringSetToStringSetMarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.marshallers.StringToStringMarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.marshallers.UUIDSetToStringSetMarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.unmarshallers.BigDecimalSetUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.unmarshallers.BigDecimalUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.unmarshallers.BigIntegerSetUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.unmarshallers.BigIntegerUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.unmarshallers.BooleanSetUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.unmarshallers.BooleanUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.unmarshallers.ByteArraySetUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.unmarshallers.ByteArrayUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.unmarshallers.ByteBufferSetUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.unmarshallers.ByteBufferUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.unmarshallers.ByteSetUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.unmarshallers.ByteUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.unmarshallers.CalendarSetUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.unmarshallers.CalendarUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.unmarshallers.CustomUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.unmarshallers.DateSetUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.unmarshallers.DateUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.unmarshallers.DoubleSetUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.unmarshallers.DoubleUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.unmarshallers.FloatSetUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.unmarshallers.FloatUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.unmarshallers.IntegerSetUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.unmarshallers.IntegerUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.unmarshallers.ListUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.unmarshallers.LongSetUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.unmarshallers.LongUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.unmarshallers.MapUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.unmarshallers.NullableUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.unmarshallers.ObjectSetUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.unmarshallers.ObjectUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.unmarshallers.S3LinkUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.unmarshallers.ShortSetUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.unmarshallers.ShortUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.unmarshallers.StringSetUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.unmarshallers.StringUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.unmarshallers.UUIDSetUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.internal.unmarshallers.UUIDUnmarshaller; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +import software.amazon.awssdk.utils.Logger; + +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +/** + * Pre-defined strategies for mapping between Java types and DynamoDB types. + */ +@SdkPublicApi +public final class ConversionSchemas { + + private static final Logger LOGGER = Logger.loggerFor(ConversionSchemas.class); + + /** + * The V1 schema mapping, which retains strict backwards compatibility with + * the original DynamoDB data model. In particular, it marshals Java + * Booleans as DynamoDB Numbers rather than the newer Boolean type, and does + * not support marshaling Lists or Maps. It can unmarshal + * values written in newer formats to ease migration. + *

+ * Use me if you have other code still using an old version of the SDK that + * does not understand the new List and Map types and want to ensure that + * you don't accidentally start writing values using these types. + */ + public static final ConversionSchema V1 = v1Builder("V1ConversionSchema").build(); + + /** + * A V2 conversion schema which retains backwards compatibility with the + * V1 conversion schema for existing DynamoDB types, but adds the ability + * to marshall recursive structures using the new List and Map types. This + * is currently the default conversion schema. + */ + public static final ConversionSchema V2_COMPATIBLE = v2CompatibleBuilder( + "V2CompatibleConversionSchema").build(); + + /** + * The native V2 conversion schema. This schema breaks compatibility with + * older versions of the mapper that only support the V1 schema by + * storing booleans as native DynamoDB Booleans rather than as a 1 or 0 + * in a DynamoDB Number. Switching to the V2 schema will prevent older + * versions of the mapper from reading items you write that contain + * booleans. + */ + public static final ConversionSchema V2 = v2Builder("V2ConversionSchema").build(); + + static final ConversionSchema DEFAULT = V2_COMPATIBLE; + + /** + * A ConversionSchema builder that defaults to building {@link #V1}. + */ + public static Builder v1Builder(String name) { + return new Builder(name, V1MarshallerSet.marshallers(), V1MarshallerSet.setMarshallers(), + StandardUnmarshallerSet.unmarshallers(), + StandardUnmarshallerSet.setUnmarshallers()); + } + + /** + * A ConversionSchema builder that defaults to building {@link #V2_COMPATIBLE}. + */ + public static Builder v2CompatibleBuilder(String name) { + return new Builder(name, V2CompatibleMarshallerSet.marshallers(), + V2CompatibleMarshallerSet.setMarshallers(), + StandardUnmarshallerSet.unmarshallers(), + StandardUnmarshallerSet.setUnmarshallers()); + } + + /** + * A ConversionSchema builder that defaults to building {@link #V2}. + */ + public static Builder v2Builder(String name) { + return new Builder(name, V2MarshallerSet.marshallers(), V2MarshallerSet.setMarshallers(), + StandardUnmarshallerSet.unmarshallers(), + StandardUnmarshallerSet.setUnmarshallers()); + } + + public static class Builder { + + private final String name; + private final List> marshallers; + private final List> setMarshallers; + private final List> unmarshallers; + private final List> setUnmarshallers; + + Builder(String name, List> marshallers, + List> setMarshallers, + List> unmarshallers, + List> setUnmarshallers) { + this.name = name; + this.marshallers = marshallers; + this.setMarshallers = setMarshallers; + this.unmarshallers = unmarshallers; + this.setUnmarshallers = setUnmarshallers; + } + + /** + * Adds marshaling of a type to the schema. Types are in LIFO order, so the last type added + * will be the first matched. + */ + public Builder addFirstType(Class clazz, ArgumentMarshaller marshaller, + ArgumentUnmarshaller unmarshaller) { + this.marshallers.add(0, Pair.of(clazz, marshaller)); + this.unmarshallers.add(0, Pair.of(clazz, unmarshaller)); + return this; + } + + /** + * Adds marshaling of a Set of a type to the schema. Types are in LIFO order, so the last + * type added will be the first matched. + */ + public Builder addFirstSetType(Class clazz, ArgumentMarshaller marshaller, + ArgumentUnmarshaller unmarshaller) { + this.setMarshallers.add(0, Pair.of(clazz, marshaller)); + this.setUnmarshallers.add(0, Pair.of(clazz, unmarshaller)); + return this; + } + + public ConversionSchema build() { + return new StandardConversionSchema(name, new AbstractMarshallerSet(marshallers, + setMarshallers), + new StandardUnmarshallerSet(unmarshallers, + setUnmarshallers)); + } + } + + static class StandardConversionSchema implements ConversionSchema { + + private final String name; + private final MarshallerSet marshallers; + private final UnmarshallerSet unmarshallers; + + public StandardConversionSchema( + String name, + MarshallerSet marshallers, + UnmarshallerSet unmarshallers) { + + this.name = name; + this.marshallers = new CachingMarshallerSet( + new AnnotationAwareMarshallerSet(marshallers)); + + this.unmarshallers = new CachingUnmarshallerSet( + new AnnotationAwareUnmarshallerSet(unmarshallers)); + } + + @Override + public ItemConverter getConverter(Dependencies dependencies) { + + S3ClientCache s3cc = dependencies.get(S3ClientCache.class); + + return new StandardItemConverter( + marshallers, + unmarshallers, + s3cc); + } + + @Override + public String toString() { + return name; + } + } + + + static class StandardItemConverter implements ItemConverter { + + private final MarshallerSet marshallerSet; + private final UnmarshallerSet unmarshallerSet; + private final S3ClientCache s3cc; + + public StandardItemConverter( + MarshallerSet marshallerSet, + UnmarshallerSet unmarshallerSet, + S3ClientCache s3cc) { + + this.marshallerSet = marshallerSet; + this.unmarshallerSet = unmarshallerSet; + this.s3cc = s3cc; + } + + @Override + public DynamoDBMapperFieldModel getFieldModel(Method getter) { + ArgumentMarshaller marshaller = getMarshaller(getter); + + DynamoDBAttributeType attributeType; + if (marshaller instanceof StringAttributeMarshaller) { + attributeType = DynamoDBAttributeType.S; + } else if (marshaller instanceof NumberAttributeMarshaller) { + attributeType = DynamoDBAttributeType.N; + } else if (marshaller instanceof BinaryAttributeMarshaller) { + attributeType = DynamoDBAttributeType.B; + } else if (marshaller instanceof StringSetAttributeMarshaller) { + attributeType = DynamoDBAttributeType.SS; + } else if (marshaller instanceof NumberSetAttributeMarshaller) { + attributeType = DynamoDBAttributeType.NS; + } else if (marshaller instanceof BinarySetAttributeMarshaller) { + attributeType = DynamoDBAttributeType.BS; + } else if (marshaller instanceof BooleanAttributeMarshaller) { + attributeType = DynamoDBAttributeType.BOOL; + } else if (marshaller instanceof ListAttributeMarshaller) { + attributeType = DynamoDBAttributeType.L; + } else if (marshaller instanceof MapAttributeMarshaller) { + attributeType = DynamoDBAttributeType.M; + } else { + throw new DynamoDBMappingException( + "Unrecognized marshaller type for " + getter + ": " + + marshaller); + } + + // Note, generating the attribute name using this method is not + // actually correct for @DynamoDBFlattened attributes, however, + // its the best that can be done given only the method. The + // proper way to get this information is using the model factory. + StandardAnnotationMaps.FieldMap annotations = StandardAnnotationMaps.of(getter, null); + DynamoDBMapperFieldModel.Builder builder = new DynamoDBMapperFieldModel.Builder(void.class, annotations); + builder.with(attributeType); + return builder.build(); + } + + @Override + public AttributeValue convert(Method getter, Object object) { + if (object == null) { + return null; + } + + ArgumentMarshaller marshaller = getMarshaller(getter); + return marshaller.marshall(object); + } + + @Override + public Map convert(Object object) { + if (object == null) { + return null; + } + + Class clazz = (Class)object.getClass(); + Map result = + new HashMap(); + + for (Bean bean : StandardBeanProperties.of(clazz).map().values()) { + Object getterResult = bean.reflect().get(object); + if (getterResult != null) { + AttributeValue value = convert(bean.type().getter(), getterResult); + if (value != null) { + result.put(bean.properties().attributeName(), value); + } + } + } + + return result; + } + + private ArgumentMarshaller getMarshaller(Method getter) { + ArgumentMarshaller marshaller = + marshallerSet.getMarshaller(getter); + + marshaller = augment(getter.getGenericReturnType(), marshaller); + + return marshaller; + } + + private ArgumentMarshaller getMemberMarshaller(Type type) { + ArgumentMarshaller marshaller = + marshallerSet.getMemberMarshaller(type); + + marshaller = augment(type, marshaller); + + return marshaller; + } + + private ArgumentMarshaller augment( + Type type, + ArgumentMarshaller marshaller) { + + if (marshaller instanceof CollectionToListMarshaller) { + return getCollectionToListMarshaller(type); + } + + if (marshaller instanceof MapToMapMarshaller) { + return getMapToMapMarshaller(type); + } + if (marshaller instanceof ObjectToMapMarshaller) { + return getObjectToMapMarshaller(type); + } + + return marshaller; + } + + private ArgumentMarshaller getCollectionToListMarshaller(Type type) { + if (!(type instanceof ParameterizedType)) { + throw new DynamoDBMappingException( + "Cannot tell what type of objects belong in the " + + "Collection type " + type + ", which is not " + + "parameterized."); + } + + ParameterizedType ptype = (ParameterizedType) type; + Type[] args = ptype.getActualTypeArguments(); + + if (args == null || args.length != 1) { + throw new DynamoDBMappingException( + "Cannot tell what type of objects belong in the " + + "Collection type " + type + "; unexpected number of " + + "type arguments."); + } + + ArgumentMarshaller memberMarshaller = + getMemberMarshaller(args[0]); + + return new CollectionToListMarshaller(memberMarshaller); + } + + private ArgumentMarshaller getMapToMapMarshaller(Type type) { + if (!(type instanceof ParameterizedType)) { + throw new DynamoDBMappingException( + "Cannot tell what type of objects belong in the Map " + + "type " + type + ", which is not parameterized."); + } + + ParameterizedType ptype = (ParameterizedType) type; + Type[] args = ptype.getActualTypeArguments(); + + if (args == null || args.length != 2) { + throw new DynamoDBMappingException( + "Cannot tell what type of objects belong in the Map " + + "type " + type + "; unexpected number of type " + + "arguments."); + } + + if (args[0] != String.class) { + throw new DynamoDBMappingException( + "Only Map is supported."); + } + + ArgumentMarshaller memberMarshaller = + getMemberMarshaller(args[1]); + + return new MapToMapMarshaller(memberMarshaller); + } + + private ArgumentMarshaller getObjectToMapMarshaller(Type type) { + Type localType = type; + if (localType instanceof ParameterizedType) { + localType = ((ParameterizedType) localType).getRawType(); + } + + if (!(localType instanceof Class)) { + throw new DynamoDBMappingException( + "Cannot convert " + type + " to a class"); + } + + Class clazz = (Class) localType; + if (StandardAnnotationMaps.of(clazz).attributeType() != DynamoDBAttributeType.M) { + throw new DynamoDBMappingException( + "Cannot marshall type " + type + + " without a custom marshaler or @DynamoDBDocument " + + "annotation."); + } + + return new ObjectToMapMarshaller(this); + } + + @Override + public Object unconvert( + Method getter, + Method setter, + AttributeValue value) { + + ArgumentUnmarshaller unmarshaller = getUnmarshaller(getter, setter); + return unmarshall(unmarshaller, setter, value); + } + + @Override + public T unconvert( + Class clazz, + Map value) { + + T result = createObject(clazz); + if (value == null || value.isEmpty()) { + return result; + } + + for (Bean bean : StandardBeanProperties.of(clazz).map().values()) { + AttributeValue av = value.get(bean.properties().attributeName()); + if (av != null) { + ArgumentUnmarshaller unmarshaller = getUnmarshaller(bean.type().getter(), bean.type().setter()); + Object unmarshalled = unmarshall(unmarshaller, bean.type().setter(), av); + bean.reflect().set(result, unmarshalled); + } + } + + return result; + } + + private ArgumentUnmarshaller getUnmarshaller( + Method getter, + Method setter) { + + ArgumentUnmarshaller unmarshaller = + unmarshallerSet.getUnmarshaller(getter, setter); + + unmarshaller = augment( + setter.getGenericParameterTypes()[0], unmarshaller); + + return new NullableUnmarshaller(unmarshaller); + } + + private ArgumentUnmarshaller getMemberUnmarshaller(Type type) { + ArgumentUnmarshaller unmarshaller = + unmarshallerSet.getMemberUnmarshaller(type); + + unmarshaller = augment(type, unmarshaller); + + return new NullableUnmarshaller(unmarshaller); + } + + private ArgumentUnmarshaller augment( + Type type, + ArgumentUnmarshaller unmarshaller) { + + // Inject our s3 client cache if it's an S3LinkUnmarshaller. + if (unmarshaller instanceof S3LinkUnmarshaller) { + return new S3LinkUnmarshaller(s3cc); + } + + // Inject an appropriate member-type unmarshaller if it's a list, + // object-set, or map unmarshaller. + if (unmarshaller instanceof ObjectSetUnmarshaller) { + return getObjectSetUnmarshaller(type); + } + + if (unmarshaller instanceof ListUnmarshaller) { + return getListUnmarshaller(type); + } + + if (unmarshaller instanceof MapUnmarshaller) { + return getMapUnmarshaller(type); + } + + // Inject ourselves to recursively unmarshall things if it's an + // ObjectUnmarshaller. + if (unmarshaller instanceof ObjectUnmarshaller) { + return getObjectUnmarshaller(type); + } + + return unmarshaller; + } + + private ArgumentUnmarshaller getObjectSetUnmarshaller(Type type) { + if (!(type instanceof ParameterizedType)) { + throw new DynamoDBMappingException( + "Cannot tell what type of objects belong in the Set " + + "type " + type + ", which is not parameterized."); + } + + ParameterizedType ptype = (ParameterizedType) type; + Type[] args = ptype.getActualTypeArguments(); + + if (args == null || args.length != 1) { + throw new DynamoDBMappingException( + "Cannot tell what type of objects belong in the Set " + + "type " + type + "; unexpected number of type " + + "arguments."); + } + + ArgumentUnmarshaller memberUnmarshaller = + getMemberUnmarshaller(args[0]); + + return new ObjectSetUnmarshaller(memberUnmarshaller); + } + + private ArgumentUnmarshaller getListUnmarshaller(Type type) { + if (!(type instanceof ParameterizedType)) { + throw new DynamoDBMappingException( + "Cannot tell what type of objects belong in the List " + + "type " + type + ", which is not parameterized."); + } + + ParameterizedType ptype = (ParameterizedType) type; + Type[] args = ptype.getActualTypeArguments(); + + if (args == null || args.length != 1) { + throw new DynamoDBMappingException( + "Cannot tell what type of objects belong in the List " + + "type " + type + "; unexpected number of type " + + "arguments."); + } + + ArgumentUnmarshaller memberUnmarshaller = + getMemberUnmarshaller(args[0]); + + return new ListUnmarshaller(memberUnmarshaller); + } + + private ArgumentUnmarshaller getMapUnmarshaller(Type type) { + if (!(type instanceof ParameterizedType)) { + throw new DynamoDBMappingException( + "Cannot tell what type of objects belong in the Map " + + "type " + type + ", which is not parameterized."); + } + + ParameterizedType ptype = (ParameterizedType) type; + Type[] args = ptype.getActualTypeArguments(); + + if (args == null || args.length != 2) { + throw new DynamoDBMappingException( + "Cannot tell what type of objects belong in the Map " + + "type " + type + "; unexpected number of type " + + "arguments."); + } + + if (args[0] != String.class) { + throw new DynamoDBMappingException( + "Only Map is supported."); + } + + ArgumentUnmarshaller memberUnmarshaller = + getMemberUnmarshaller(args[1]); + + return new MapUnmarshaller(memberUnmarshaller); + } + + private ArgumentUnmarshaller getObjectUnmarshaller(Type type) { + Type localType = type; + if (localType instanceof ParameterizedType) { + localType = ((ParameterizedType) type).getRawType(); + } + + if (!(localType instanceof Class)) { + throw new DynamoDBMappingException( + "Cannot convert " + type + " to a class"); + } + + Class clazz = (Class) localType; + if (StandardAnnotationMaps.of(clazz).attributeType() != DynamoDBAttributeType.M) { + throw new DynamoDBMappingException( + "Cannot unmarshall to type " + type + + " without a custom marshaler or @DynamoDBDocument " + + "annotation."); + } + + return new ObjectUnmarshaller(this, clazz); + } + + private static Object unmarshall( + ArgumentUnmarshaller unmarshaller, + Method setter, + AttributeValue value) { + + unmarshaller.typeCheck(value, setter); + + try { + + return unmarshaller.unmarshall(value); + + } catch (IllegalArgumentException e) { + throw new DynamoDBMappingException( + "Couldn't unmarshall value " + value + " for " + setter, + e); + + } catch (ParseException e) { + throw new DynamoDBMappingException( + "Error attempting to parse date string " + value + " for " + + setter, + e); + } + } + + private static T createObject(Class clazz) { + try { + + return clazz.newInstance(); + + } catch (InstantiationException e) { + throw new DynamoDBMappingException( + "Failed to instantiate new instance of class", e); + + } catch (IllegalAccessException e) { + throw new DynamoDBMappingException( + "Failed to instantiate new instance of class", e); + } + } + + } + + static interface MarshallerSet { + ArgumentMarshaller getMarshaller(Method getter); + ArgumentMarshaller getMemberMarshaller(Type memberType); + } + + static interface UnmarshallerSet { + ArgumentUnmarshaller getUnmarshaller(Method getter, Method setter); + ArgumentUnmarshaller getMemberUnmarshaller(Type memberType); + } + + static final class V2MarshallerSet { + + private static List> marshallers() { + List> list = + new ArrayList>(); + + // Use the new V2 boolean marshallers. + addStandardDateMarshallers(list); + addV2BooleanMarshallers(list); + addStandardNumberMarshallers(list); + addStandardStringMarshallers(list); + addStandardBinaryMarshallers(list); + addStandardS3LinkMarshallers(list); + + // Add marshallers for the new list and map types. + list.add(Pair.of(List.class, CollectionToListMarshaller.instance())); + list.add(Pair.of(Map.class, MapToMapMarshaller.instance())); + + // Make sure I'm last since I'll catch anything. + list.add(Pair.of(Object.class, ObjectToMapMarshaller.instance())); + + return list; + } + + private static List> setMarshallers() { + List> list = + new ArrayList>(); + + // No more Set -> NS or Set -> SS marshallers + addStandardDateSetMarshallers(list); + addStandardNumberSetMarshallers(list); + addStandardStringSetMarshallers(list); + addStandardBinarySetMarshallers(list); + + // Make sure I'm last since I'll catch anything. + list.add(Pair.of( + Object.class, + CollectionToListMarshaller.instance())); + + return list; + } + } + + static final class V2CompatibleMarshallerSet { + + private static List> marshallers() { + List> list = + new ArrayList>(); + + // Keep the old v1 boolean marshallers for compatibility. + addStandardDateMarshallers(list); + addV1BooleanMarshallers(list); + addStandardNumberMarshallers(list); + addStandardStringMarshallers(list); + addStandardBinaryMarshallers(list); + addStandardS3LinkMarshallers(list); + + // Add marshallers for the new list and map types. + list.add(Pair.of(List.class, CollectionToListMarshaller.instance())); + list.add(Pair.of(Map.class, MapToMapMarshaller.instance())); + + // Make sure I'm last since I'll catch anything. + list.add(Pair.of(Object.class, ObjectToMapMarshaller.instance())); + + return list; + } + + private static List> setMarshallers() { + List> list = + new ArrayList>(); + + addStandardDateSetMarshallers(list); + addV1BooleanSetMarshallers(list); + addStandardNumberSetMarshallers(list); + addStandardStringSetMarshallers(list); + addStandardBinarySetMarshallers(list); + + // If all else fails, fall back to this default marshaler to + // retain backwards-compatible behavior. + list.add(Pair.of(Object.class, ObjectSetToStringSetMarshaller.instance())); + + return list; + } + } + + static final class V1MarshallerSet { + + private static List> marshallers() { + List> list = + new ArrayList>(); + + addStandardDateMarshallers(list); + addV1BooleanMarshallers(list); + addStandardNumberMarshallers(list); + addStandardStringMarshallers(list); + addStandardBinaryMarshallers(list); + addStandardS3LinkMarshallers(list); + + return list; + } + + private static List> setMarshallers() { + List> list = + new ArrayList>(); + + addStandardDateSetMarshallers(list); + addV1BooleanSetMarshallers(list); + addStandardNumberSetMarshallers(list); + addStandardStringSetMarshallers(list); + addStandardBinarySetMarshallers(list); + + // If all else fails, fall back to this default marshaler to + // retain backwards-compatible behavior. + list.add(Pair.of(Object.class, + ObjectSetToStringSetMarshaller.instance())); + + return list; + } + } + + private static void addStandardDateMarshallers( + List> list) { + + list.add(Pair.of(Date.class, + DateToStringMarshaller.instance())); + list.add(Pair.of(Calendar.class, + CalendarToStringMarshaller.instance())); + } + + private static void addV1BooleanMarshallers( + List> list) { + + list.add(Pair.of(Boolean.class, + BooleanToNumberMarshaller.instance())); + list.add(Pair.of(boolean.class, + BooleanToNumberMarshaller.instance())); + } + + private static void addV2BooleanMarshallers( + List> list) { + + list.add(Pair.of(Boolean.class, + BooleanToBooleanMarshaller.instance())); + list.add(Pair.of(boolean.class, + BooleanToBooleanMarshaller.instance())); + } + + private static void addStandardNumberMarshallers( + List> list) { + + list.add(Pair.of(Number.class, + NumberToNumberMarshaller.instance())); + list.add(Pair.of(byte.class, + NumberToNumberMarshaller.instance())); + list.add(Pair.of(short.class, + NumberToNumberMarshaller.instance())); + list.add(Pair.of(int.class, + NumberToNumberMarshaller.instance())); + list.add(Pair.of(long.class, + NumberToNumberMarshaller.instance())); + list.add(Pair.of(float.class, + NumberToNumberMarshaller.instance())); + list.add(Pair.of(double.class, + NumberToNumberMarshaller.instance())); + } + + private static void addStandardStringMarshallers( + List> list) { + + list.add(Pair.of(String.class, + StringToStringMarshaller.instance())); + + list.add(Pair.of(UUID.class, + ObjectToStringMarshaller.instance())); + } + + private static void addStandardBinaryMarshallers( + List> list) { + + list.add(Pair.of(ByteBuffer.class, + ByteBufferToBinaryMarshaller.instance())); + list.add(Pair.of(byte[].class, + ByteArrayToBinaryMarshaller.instance())); + } + + private static void addStandardS3LinkMarshallers( + List> list) { + + list.add(Pair.of(S3Link.class, + S3LinkToStringMarshaller.instance())); + } + + private static void addStandardDateSetMarshallers( + List> list) { + + list.add(Pair.of(Date.class, + DateSetToStringSetMarshaller.instance())); + list.add(Pair.of(Calendar.class, + CalendarSetToStringSetMarshaller.instance())); + } + + private static void addStandardNumberSetMarshallers( + List> list) { + + list.add(Pair.of(Number.class, + NumberSetToNumberSetMarshaller.instance())); + list.add(Pair.of(byte.class, + NumberSetToNumberSetMarshaller.instance())); + list.add(Pair.of(short.class, + NumberSetToNumberSetMarshaller.instance())); + list.add(Pair.of(int.class, + NumberSetToNumberSetMarshaller.instance())); + list.add(Pair.of(long.class, + NumberSetToNumberSetMarshaller.instance())); + list.add(Pair.of(float.class, + NumberSetToNumberSetMarshaller.instance())); + list.add(Pair.of(double.class, + NumberSetToNumberSetMarshaller.instance())); + } + + private static void addStandardStringSetMarshallers( + List> list) { + + list.add(Pair.of(String.class, + StringSetToStringSetMarshaller.instance())); + + list.add(Pair.of(UUID.class, + UUIDSetToStringSetMarshaller.instance())); + } + + private static void addStandardBinarySetMarshallers( + List> list) { + + list.add(Pair.of(ByteBuffer.class, + ByteBufferSetToBinarySetMarshaller.instance())); + list.add(Pair.of(byte[].class, + ByteArraySetToBinarySetMarshaller.instance())); + } + + private static void addV1BooleanSetMarshallers( + List> list) { + + list.add(Pair.of(Boolean.class, + BooleanSetToNumberSetMarshaller.instance())); + list.add(Pair.of(boolean.class, + BooleanSetToNumberSetMarshaller.instance())); + } + + private static class AbstractMarshallerSet implements MarshallerSet { + + private final List> marshallers; + private final List> setMarshallers; + + public AbstractMarshallerSet( + List> marshallers, + List> setMarshallers) { + + this.marshallers = marshallers; + this.setMarshallers = setMarshallers; + } + + @Override + public ArgumentMarshaller getMarshaller(Method getter) { + Class returnType = getter.getReturnType(); + + if (Set.class.isAssignableFrom(returnType)) { + Class memberType = + unwrapGenericSetParam(getter.getGenericReturnType()); + + return getSet(getter, memberType); + } else { + return getScalar(getter, returnType); + } + } + + @Override + public ArgumentMarshaller getMemberMarshaller(Type memberType) { + Class clazz = resolveClass(memberType); + if (Set.class.isAssignableFrom(clazz)) { + Class setMemberType = unwrapGenericSetParam(memberType); + return getSet(null, setMemberType); + } else { + return getScalar(null, clazz); + } + } + + private ArgumentMarshaller getScalar(Method getter, Class type) { + ArgumentMarshaller marshaller = find(type, marshallers); + if (marshaller == null) { + + String className = "?"; + String methodName = "?"; + if (getter != null) { + className = getter.getDeclaringClass().toString(); + methodName = getter.getName(); + } + + throw new DynamoDBMappingException( + "Cannot marshall return type " + type + + " of method " + className + "." + methodName + + " without a custom marshaler."); + } + + return marshaller; + } + + private ArgumentMarshaller getSet(Method getter, Class memberType) { + ArgumentMarshaller marshaller = find(memberType, setMarshallers); + if (marshaller == null) { + + String className = "?"; + String methodName = "?"; + if (getter != null) { + className = getter.getDeclaringClass().toString(); + methodName = getter.getName(); + } + + throw new DynamoDBMappingException( + "Cannot marshall return type Set<" + memberType + + "> of method " + className + "." + methodName + + " without a custom marshaller."); + } + + return marshaller; + } + } + + static class StandardUnmarshallerSet implements UnmarshallerSet { + + private final List> unmarshallers; + private final List> setUnmarshallers; + + public StandardUnmarshallerSet() { + this(unmarshallers(), setUnmarshallers()); + } + + public StandardUnmarshallerSet( + List> unmarshallers, + List> setUnmarshallers) { + + this.unmarshallers = unmarshallers; + this.setUnmarshallers = setUnmarshallers; + } + + private static List> unmarshallers() { + List> list = + new ArrayList>(); + + list.add(Pair.of(double.class, DoubleUnmarshaller.instance())); + list.add(Pair.of(Double.class, DoubleUnmarshaller.instance())); + + list.add(Pair.of(BigDecimal.class, + BigDecimalUnmarshaller.instance())); + list.add(Pair.of(BigInteger.class, + BigIntegerUnmarshaller.instance())); + + list.add(Pair.of(int.class, IntegerUnmarshaller.instance())); + list.add(Pair.of(Integer.class, IntegerUnmarshaller.instance())); + + list.add(Pair.of(float.class, FloatUnmarshaller.instance())); + list.add(Pair.of(Float.class, FloatUnmarshaller.instance())); + + list.add(Pair.of(byte.class, ByteUnmarshaller.instance())); + list.add(Pair.of(Byte.class, ByteUnmarshaller.instance())); + + list.add(Pair.of(long.class, LongUnmarshaller.instance())); + list.add(Pair.of(Long.class, LongUnmarshaller.instance())); + + list.add(Pair.of(short.class, ShortUnmarshaller.instance())); + list.add(Pair.of(Short.class, ShortUnmarshaller.instance())); + + list.add(Pair.of(boolean.class, BooleanUnmarshaller.instance())); + list.add(Pair.of(Boolean.class, BooleanUnmarshaller.instance())); + + list.add(Pair.of(Date.class, DateUnmarshaller.instance())); + list.add(Pair.of(Calendar.class, CalendarUnmarshaller.instance())); + + list.add(Pair.of(ByteBuffer.class, + ByteBufferUnmarshaller.instance())); + list.add(Pair.of(byte[].class, + ByteArrayUnmarshaller.instance())); + + list.add(Pair.of(S3Link.class, S3LinkUnmarshaller.instance())); + list.add(Pair.of(UUID.class, UUIDUnmarshaller.instance())); + list.add(Pair.of(String.class, StringUnmarshaller.instance())); + + list.add(Pair.of(List.class, ListUnmarshaller.instance())); + list.add(Pair.of(Map.class, MapUnmarshaller.instance())); + + // Make sure I'm last since I'll catch all other types. + list.add(Pair.of(Object.class, ObjectUnmarshaller.instance())); + + return list; + } + + private static List> setUnmarshallers() { + List> list = + new ArrayList>(); + + list.add(Pair.of(double.class, DoubleSetUnmarshaller.instance())); + list.add(Pair.of(Double.class, DoubleSetUnmarshaller.instance())); + + list.add(Pair.of(BigDecimal.class, + BigDecimalSetUnmarshaller.instance())); + list.add(Pair.of(BigInteger.class, + BigIntegerSetUnmarshaller.instance())); + + list.add(Pair.of(int.class, IntegerSetUnmarshaller.instance())); + list.add(Pair.of(Integer.class, IntegerSetUnmarshaller.instance())); + + list.add(Pair.of(float.class, FloatSetUnmarshaller.instance())); + list.add(Pair.of(Float.class, FloatSetUnmarshaller.instance())); + + list.add(Pair.of(byte.class, ByteSetUnmarshaller.instance())); + list.add(Pair.of(Byte.class, ByteSetUnmarshaller.instance())); + + list.add(Pair.of(long.class, LongSetUnmarshaller.instance())); + list.add(Pair.of(Long.class, LongSetUnmarshaller.instance())); + + list.add(Pair.of(short.class, ShortSetUnmarshaller.instance())); + list.add(Pair.of(Short.class, ShortSetUnmarshaller.instance())); + + list.add(Pair.of(boolean.class, BooleanSetUnmarshaller.instance())); + list.add(Pair.of(Boolean.class, BooleanSetUnmarshaller.instance())); + + list.add(Pair.of(Date.class, DateSetUnmarshaller.instance())); + list.add(Pair.of(Calendar.class, + CalendarSetUnmarshaller.instance())); + + list.add(Pair.of(ByteBuffer.class, + ByteBufferSetUnmarshaller.instance())); + list.add(Pair.of(byte[].class, + ByteArraySetUnmarshaller.instance())); + + list.add(Pair.of(UUID.class, UUIDSetUnmarshaller.instance())); + list.add(Pair.of(String.class, StringSetUnmarshaller.instance())); + + // Make sure I'm last since I'll catch all other types. + list.add(Pair.of(Object.class, ObjectSetUnmarshaller.instance())); + + return list; + } + + @Override + public ArgumentUnmarshaller getUnmarshaller( + Method getter, + Method setter) { + + if (setter.getParameterTypes().length != 1) { + throw new DynamoDBMappingException( + "Expected exactly one agument to " + setter); + } + Class paramType = setter.getParameterTypes()[0]; + + if (Set.class.isAssignableFrom(paramType)) { + + paramType = unwrapGenericSetParam( + setter.getGenericParameterTypes()[0]); + + return getSet(setter, paramType); + + } else { + return getScalar(setter, paramType); + } + } + + @Override + public ArgumentUnmarshaller getMemberUnmarshaller(Type memberType) { + Class clazz = resolveClass(memberType); + if (Set.class.isAssignableFrom(clazz)) { + Class setMemberType = unwrapGenericSetParam(memberType); + return getSet(null, setMemberType); + } else { + return getScalar(null, clazz); + } + } + + private ArgumentUnmarshaller getSet(Method setter, Class paramType) { + ArgumentUnmarshaller unmarshaller = + find(paramType, setUnmarshallers); + + String className = "?"; + String methodName = "?"; + if (setter != null) { + className = setter.getDeclaringClass().toString(); + methodName = setter.getName(); + } + + if (unmarshaller == null) { + throw new DynamoDBMappingException( + "Cannot unmarshall to parameter type Set<" + + paramType + "> of method " + + className + "." + methodName + " without a custom " + + "unmarshaler."); + } + + return unmarshaller; + } + + private ArgumentUnmarshaller getScalar(Method setter, Class type) { + ArgumentUnmarshaller unmarshaller = find(type, unmarshallers); + + String className = "?"; + String methodName = "?"; + if (setter != null) { + className = setter.getDeclaringClass().toString(); + methodName = setter.getName(); + } + + if (unmarshaller == null) { + throw new DynamoDBMappingException( + "Cannot unmarshall to parameter type " + type + + "of method " + className + "." + methodName + + " without a custom unmarshaler."); + } + + return unmarshaller; + } + } + + private static Class unwrapGenericSetParam(Type setType) { + if (!(setType instanceof ParameterizedType)) { + LOGGER.warn(() -> "Set type " + setType + " is not a " + + "ParameterizedType, using default marshaler and " + + "unmarshaler!"); + return Object.class; + } + + ParameterizedType ptype = (ParameterizedType) setType; + Type[] arguments = ptype.getActualTypeArguments(); + + if (arguments.length != 1) { + LOGGER.warn(() -> "Set type " + setType + " does not have exactly one " + + "type argument, using default marshaler and " + + "unmarshaler!"); + return Object.class; + } + + if (arguments[0].toString().equals("byte[]")) { + return byte[].class; + } else { + return (Class) arguments[0]; + } + } + + private static Class resolveClass(Type type) { + Type localType = type; + if (localType instanceof ParameterizedType) { + localType = ((ParameterizedType) type).getRawType(); + } + if (!(localType instanceof Class)) { + throw new DynamoDBMappingException("Cannot resolve class for type " + + type); + } + return (Class) localType; + } + + private static T find(Class needle, List> haystack) { + for (Pair pair : haystack) { + if (pair.key.isAssignableFrom(needle)) { + return pair.value; + } + } + return null; + } + + private static class Pair { + + public static Pair of ( + Class key, + ArgumentMarshaller value) { + + return new Pair(key, value); + } + + public static Pair of ( + Class key, + ArgumentUnmarshaller value) { + + return new Pair(key, value); + } + + public final Class key; + public final T value; + + private Pair(Class key, T value) { + this.key = key; + this.value = value; + } + } + + static class AnnotationAwareMarshallerSet + implements MarshallerSet { + + private final MarshallerSet wrapped; + + public AnnotationAwareMarshallerSet(MarshallerSet wrapped) { + this.wrapped = wrapped; + } + + @Override + public ArgumentMarshaller getMarshaller(Method getter) { + StandardAnnotationMaps.FieldMap annotations = StandardAnnotationMaps.of(getter, null); + DynamoDBMarshalling marshalling = annotations.actualOf(DynamoDBMarshalling.class); + if (marshalling != null) { + return new CustomMarshaller(marshalling.marshallerClass()); + } else if (annotations.actualOf(DynamoDBNativeBoolean.class) != null) { + return BooleanToBooleanMarshaller.instance(); + } + return wrapped.getMarshaller(getter); + } + + @Override + public ArgumentMarshaller getMemberMarshaller(Type memberType) { + return wrapped.getMemberMarshaller(memberType); + } + } + + static class AnnotationAwareUnmarshallerSet + implements UnmarshallerSet { + + private final UnmarshallerSet wrapped; + + public AnnotationAwareUnmarshallerSet(UnmarshallerSet wrapped) { + this.wrapped = wrapped; + } + + @Override + public ArgumentUnmarshaller getUnmarshaller( + Method getter, + Method setter) { + StandardAnnotationMaps.FieldMap annotations = StandardAnnotationMaps.of(getter, null); + DynamoDBMarshalling marshalling = annotations.actualOf(DynamoDBMarshalling.class); + if (marshalling != null) { + return new CustomUnmarshaller(getter.getReturnType(), marshalling.marshallerClass()); + } + return wrapped.getUnmarshaller(getter, setter); + } + + @Override + public ArgumentUnmarshaller getMemberUnmarshaller(Type c) { + return wrapped.getMemberUnmarshaller(c); + } + } + + static class CachingMarshallerSet implements MarshallerSet { + + private final Map cache = + new HashMap(); + + private final Map memberCache = + new HashMap(); + + private final MarshallerSet wrapped; + + public CachingMarshallerSet(MarshallerSet wrapped) { + this.wrapped = wrapped; + } + + @Override + public ArgumentMarshaller getMarshaller(Method getter) { + synchronized (cache) { + ArgumentMarshaller marshaler = cache.get(getter); + if (marshaler != null) { + return marshaler; + } + + marshaler = wrapped.getMarshaller(getter); + cache.put(getter, marshaler); + return marshaler; + } + } + + @Override + public ArgumentMarshaller getMemberMarshaller(Type memberType) { + synchronized (memberCache) { + ArgumentMarshaller marshaller = memberCache.get(memberType); + if (marshaller != null) { + return marshaller; + } + + marshaller = wrapped.getMemberMarshaller(memberType); + memberCache.put(memberType, marshaller); + return marshaller; + } + } + } + + static class CachingUnmarshallerSet implements UnmarshallerSet { + + private final Map cache = + new HashMap(); + + private final Map memberCache = + new HashMap(); + + private final UnmarshallerSet wrapped; + + public CachingUnmarshallerSet(UnmarshallerSet wrapped) { + this.wrapped = wrapped; + } + + @Override + public ArgumentUnmarshaller getUnmarshaller( + Method getter, + Method setter) { + + synchronized (cache) { + ArgumentUnmarshaller unmarshaler = cache.get(getter); + if (unmarshaler != null) { + return unmarshaler; + } + + unmarshaler = wrapped.getUnmarshaller(getter, setter); + cache.put(getter, unmarshaler); + return unmarshaler; + } + } + + @Override + public ArgumentUnmarshaller getMemberUnmarshaller(Type memberType) { + synchronized (memberCache) { + ArgumentUnmarshaller unmarshaller = memberCache.get(memberType); + if (unmarshaller != null) { + return unmarshaller; + } + + unmarshaller = wrapped.getMemberUnmarshaller(memberType); + memberCache.put(memberType, unmarshaller); + return unmarshaller; + } + } + } + + /** + * {@link AttributeValue} converter with {@link ItemConverter} + */ + static class ItemConverterRuleFactory implements RuleFactory { + private final RuleFactory typeConverters; + private final ItemConverter converter; + private final boolean customSchema; + + ItemConverterRuleFactory(DynamoDBMapperConfig config, S3Link.Factory s3Links, RuleFactory typeConverters) { + ConversionSchema.Dependencies depends = new ConversionSchema.Dependencies().with(S3ClientCache.class, s3Links.getS3ClientCache()); + ConversionSchema schema = config.getConversionSchema(); + + this.customSchema = (schema != V1 && schema != V2_COMPATIBLE && schema != V2); + this.converter = schema.getConverter(depends); + this.typeConverters = typeConverters; + } + + @Override + public Rule getRule(ConvertibleType type) { + if (customSchema && type.typeConverter() == null) { + return new ItemConverterRule(type); + } else { + return typeConverters.getRule(type); + } + } + + private final class ItemConverterRule implements Rule, DynamoDBTypeConverter { + private final ConvertibleType type; + private ItemConverterRule(final ConvertibleType type) { + this.type = type; + } + @Override + public boolean isAssignableFrom(ConvertibleType type) { + return true; + } + @Override + public DynamoDBTypeConverter newConverter(ConvertibleType type) { + return this; + } + @Override + public DynamoDBAttributeType getAttributeType() { + try { + return converter.getFieldModel(type.getter()).attributeType(); + } catch (final DynamoDBMappingException no) {} + return DynamoDBAttributeType.NULL; + } + @Override + public AttributeValue convert(final V object) { + return converter.convert(type.getter(), object); + } + @Override + public V unconvert(final AttributeValue object) { + return (V)converter.unconvert(type.getter(), type.setter(), object); + } + } + } + + private ConversionSchemas() { + throw new UnsupportedOperationException(); + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/ConvertibleType.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/ConvertibleType.java new file mode 100644 index 000000000000..0de8168d12c2 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/ConvertibleType.java @@ -0,0 +1,217 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperFieldModel.DynamoDBAttributeType; +import software.amazon.awssdk.mapper.dynamodb.StandardAnnotationMaps.TypedMap; +import software.amazon.awssdk.mapper.dynamodb.StandardTypeConverters.Scalar; +import software.amazon.awssdk.mapper.dynamodb.StandardTypeConverters.Vector; +import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType; + +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +/** + * Generic type helper. + */ +@SdkInternalApi +final class ConvertibleType { + + private final DynamoDBTypeConverter typeConverter; + private final DynamoDBAttributeType attributeType; + private final ConvertibleType[] params; + private final Class targetType; + + @Deprecated + private final Method getter, setter; + + /** + * Constructs a new parameter type. + */ + @SuppressWarnings("unchecked") + private ConvertibleType(Type genericType, TypedMap annotations, Method getter) { + this.typeConverter = annotations.typeConverter(); + this.attributeType = annotations.attributeType(); + + if (typeConverter != null) { + ConvertibleType target = ConvertibleType.of(typeConverter); + this.targetType = target.targetType; + this.params = target.params; + } else if (genericType instanceof ParameterizedType) { + Type[] paramTypes = ((ParameterizedType)genericType).getActualTypeArguments(); + this.targetType = annotations.targetType(); + this.params = new ConvertibleType[paramTypes.length]; + for (int i = 0; i < paramTypes.length; i++) { + this.params[i] = ConvertibleType.of(paramTypes[i]); + } + } else { + this.targetType = annotations.targetType(); + this.params = new ConvertibleType[0]; + } + + this.setter = getter == null ? null : StandardBeanProperties.MethodReflect.setterOf(getter); + this.getter = getter; + } + + /** + * Gets the target custom type-converter. + */ + final DynamoDBTypeConverter typeConverter() { + return (DynamoDBTypeConverter)this.typeConverter; + } + + /** + * Gets the overriding attribute type. + */ + final DynamoDBAttributeType attributeType() { + return this.attributeType; + } + + /** + * Gets the getter method. + */ + @Deprecated + final Method getter() { + return this.getter; + } + + /** + * Gets the setter method. + */ + @Deprecated + final Method setter() { + return this.setter; + } + + /** + * Gets the scalar parameter types. + */ + final ConvertibleType param(final int index) { + return this.params.length > index ? (ConvertibleType)this.params[index] : null; + } + + /** + * Returns true if the types match. + */ + final boolean is(ScalarAttributeType scalarAttributeType, Vector vector) { + return param(0) != null && param(0).is(scalarAttributeType) && is(vector); + } + + /** + * Returns true if the types match. + */ + final boolean is(ScalarAttributeType scalarAttributeType) { + return Scalar.of(targetType()).is(scalarAttributeType); + } + + /** + * Returns true if the types match. + */ + final boolean is(Scalar scalar) { + return scalar.is(targetType()); + } + + /** + * Returns true if the types match. + */ + final boolean is(Vector vector) { + return vector.is(targetType()); + } + + /** + * Returns true if the types match. + */ + final boolean is(Class type) { + return type.isAssignableFrom(targetType()); + } + + /** + * Gets the raw scalar type. + */ + final Class targetType() { + return this.targetType; + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + StringBuilder builder = new StringBuilder(targetType().getSimpleName()); + if (this.params.length > 0) { + builder.append("<"); + for (int i = 0; i < this.params.length; i++) { + builder.append(i == 0 ? "" : ",").append(this.params[i]); + } + builder.append(">"); + } + return builder.toString(); + } + + /** + * Returns the conversion type for the method and annotations. + */ + static ConvertibleType of(Method getter, TypedMap annotations) { + return new ConvertibleType(getter.getGenericReturnType(), annotations, getter); + } + + /** + * Returns the conversion type for the converter. + */ + private static ConvertibleType of(final DynamoDBTypeConverter converter) { + Class clazz = converter.getClass(); + if (!clazz.isInterface()) { + for (Class c = clazz; Object.class != c; c = c.getSuperclass()) { + for (Type genericType : c.getGenericInterfaces()) { + ConvertibleType type = ConvertibleType.of(genericType); + if (type.is(DynamoDBTypeConverter.class)) { + if (type.params.length == 2 && type.param(0).targetType() != Object.class) { + return type.param(0); + } + } + } + } + ConvertibleType type = ConvertibleType.of(clazz.getGenericSuperclass()); + if (type.is(DynamoDBTypeConverter.class)) { + if (type.params.length > 0 && type.param(0).targetType() != Object.class) { + return type.param(0); + } + } + } + throw new DynamoDBMappingException("could not resolve type of " + clazz); + } + + /** + * Returns the conversion type for the generic type. + */ + private static ConvertibleType of(Type genericType) { + Class targetType; + if (genericType instanceof Class) { + targetType = (Class)genericType; + } else if (genericType instanceof ParameterizedType) { + targetType = (Class)((ParameterizedType)genericType).getRawType(); + } else if (genericType.toString().equals("byte[]")) { + targetType = (Class)byte[].class; + } else { + targetType = (Class)Object.class; + } + TypedMap annotations = StandardAnnotationMaps.of(targetType); + return new ConvertibleType(genericType, annotations, null); + } + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDB.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDB.java new file mode 100644 index 000000000000..ba936993f406 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDB.java @@ -0,0 +1,29 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.mapper.dynamodb; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to mark other annotations as being part of DyanmoDB. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.ANNOTATION_TYPE) +public @interface DynamoDB { +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBAttribute.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBAttribute.java new file mode 100644 index 000000000000..df9a924cae66 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBAttribute.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.mapper.dynamodb; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Interface for marking a class property as an attribute in a DynamoDB table. + * Applied to the getter method or the class field for a modeled property. If + * the annotation is applied directly to the class field, the corresponding + * getter and setter must be declared in the same class. + *

+ * This annotation is optional when the name of the DynamoDB attribute matches + * the name of the property declared in the class. When they differ, use this + * annotation with the attributeName() parameter to specify which DynamoDB + * attribute this property corresponds to. Furthermore, the + * {@link DynamoDBMapper} class assumes Java naming conventions, and will + * lower-case the first character of a getter method's property name to + * determine the name of the property. E.g., a method getValue() will map to the + * DynamoDB attribute "value". Similarly, a method isValid() maps to the + * DynamoDB attribute "valid". + *

+ * Getter methods not marked with this annotation are assumed to be modeled + * properties, unless marked with {@link DynamoDBIgnore}. + */ +@DynamoDB +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.METHOD}) +public @interface DynamoDBAttribute { + + /** + * Optional parameter when the name of the attribute as stored in DynamoDB + * should differ from the name used by the getter / setter. + */ + String attributeName() default ""; + + /** + * Optional parameter when using {@link DynamoDBFlattened}; identifies + * the field/property name on the target class to map as the attribute. + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBFlattened + */ + String mappedBy() default ""; +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBAutoGenerateStrategy.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBAutoGenerateStrategy.java new file mode 100644 index 000000000000..fc4285c35215 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBAutoGenerateStrategy.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.mapper.dynamodb; + +/** + * Enumeration of possible auto-generation strategies. + * @see DynamoDBAutoGeneratedTimestamp + */ +public enum DynamoDBAutoGenerateStrategy { + + /** + * Instructs to always generate both on create and update. + */ + ALWAYS, + + /** + * Instructs to generate on create only. + */ + CREATE; + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBAutoGenerated.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBAutoGenerated.java new file mode 100644 index 000000000000..d95e6184ec03 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBAutoGenerated.java @@ -0,0 +1,83 @@ +/* + * 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.mapper.dynamodb; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to mark a property as using a custom auto-generator. + * + *

May be annotated on a user-defined annotation to pass additional + * properties to the {@link DynamoDBAutoGenerator}.

+ * + *
+ * @DynamoDBHashKey
+ * @CustomGeneratedKey(prefix="test-") //<- user-defined annotation
+ * public String getKey()
+ * 
+ * + *

Where,

+ *
+ * @DynamoDBAutoGenerated(generator=CustomGeneratedKey.Generator.class)
+ * @Retention(RetentionPolicy.RUNTIME)
+ * @Target({ElementType.METHOD})
+ * public @interface CustomGeneratedKey {
+ *     String prefix() default "";
+ *
+ *     public static class Generator implements DynamoDBAutoGenerator<String> {
+ *         private final String prefix;
+ *         public Generator(final Class<String> targetType, final CustomGeneratedKey annotation) {
+ *             this.prefix = annotation.prefix();
+ *         }
+ *         public Generator() { //<- required if annotating directly
+ *             this.prefix = "";
+ *         }
+ *         @Override
+ *         public DynamoDBAutoGenerateStrategy getGenerateStrategy() {
+ *             return DynamoDBAutoGenerateStrategy.CREATE;
+ *         }
+ *         @Override
+ *         public final String generate(final String currentValue) {
+ *             return prefix + UUID.randomUUID.toString();
+ *         }
+ *     }
+ * }
+ * 
+ * + *

Alternately, the property/field may be annotated directly (which requires + * the generator to provide a default constructor),

+ *
+ * @DynamoDBAutoGenerated(generator=CustomGeneratedKey.Generator.class)
+ * public String getKey()
+ * 
+ * + *

May be used as a meta-annotation.

+ */ +@DynamoDB +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +public @interface DynamoDBAutoGenerated { + + /** + * The auto-generator class for this property. + */ + @SuppressWarnings("rawtypes") + Class generator(); + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBAutoGeneratedDefault.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBAutoGeneratedDefault.java new file mode 100644 index 000000000000..c60ec6a66c61 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBAutoGeneratedDefault.java @@ -0,0 +1,66 @@ +/* + * 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.mapper.dynamodb; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to assign a default value on creation if value is null. + * + *
+ * @DynamoDBAutoGeneratedDefault("OPEN")
+ * public String getStatus()
+ * 
+ * + *

Only compatible with standard string types.

+ * + */ +@DynamoDB +@DynamoDBAutoGenerated(generator=DynamoDBAutoGeneratedDefault.Generator.class) +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.METHOD}) +public @interface DynamoDBAutoGeneratedDefault { + + /** + * The default value. + */ + String value(); + + + /** + * Default generator. + */ + static final class Generator extends DynamoDBAutoGenerator.AbstractGenerator { + private final DynamoDBTypeConverter converter; + private final String defaultValue; + + public Generator(Class targetType, DynamoDBAutoGeneratedDefault annotation) { + super(DynamoDBAutoGenerateStrategy.CREATE); + this.converter = StandardTypeConverters.factory().getConverter(targetType, String.class); + this.defaultValue = annotation.value(); + } + + @Override + public final T generate(T currentValue) { + return converter.convert(defaultValue); + } + } + + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBAutoGeneratedKey.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBAutoGeneratedKey.java new file mode 100644 index 000000000000..d6bd185953b0 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBAutoGeneratedKey.java @@ -0,0 +1,40 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.mapper.dynamodb; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation for marking a hash key or range key property in a class to + * auto-generate this key. Only String typed keys can be auto generated, and are + * given a random UUID. The annotation can be applied to either the getter + * method or the class field for the auto-generated key property. If the + * annotation is applied directly to the class field, the corresponding getter + * and setter must be declared in the same class. This annotation can be applied + * to both primary and index keys. + * + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBGeneratedUuid + * @see java.util.UUID + */ +@DynamoDBGeneratedUuid(DynamoDBAutoGenerateStrategy.CREATE) +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.METHOD}) +public @interface DynamoDBAutoGeneratedKey { + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBAutoGeneratedTimestamp.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBAutoGeneratedTimestamp.java new file mode 100644 index 000000000000..cece970d8bd3 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBAutoGeneratedTimestamp.java @@ -0,0 +1,73 @@ +/* + * 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.mapper.dynamodb; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Date; + +/** + * Annotation for auto-generating a date/timestamp. + * + *
+ * @DynamoDBAutoGeneratedTimestamp(strategy=DynamoDBAutoGenerateStrategy.CREATE)
+ * public Date getCreatedDate() { return createdDate; }
+ * public void setCreatedDate(Date createdDate) { this.createdDate = createdDate; }
+ *
+ * @DynamoDBAutoGeneratedTimestamp(strategy=DynamoDBAutoGenerateStrategy.ALWAYS)
+ * public Date getLastUpdatedDate() { return lastUpdatedDate; }
+ * public void setLastUpdatedDate(Date lastUpdatedDate) { this.lastUpdatedDate = lastUpdatedDate; }
+ * 
+ * + *

Supports the standard {@link Date} type-conversions; such as + * {@link java.util.Calendar}, {@link Long}.

+ * + *

Primitives such as {@code long} are not supported since the unset + * (or null) state can't be detected.

+ * + *

Compatible with {@link DynamoDBTypeConvertedTimestamp}

+ */ +@DynamoDBAutoGenerated(generator=DynamoDBAutoGeneratedTimestamp.Generator.class) +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.METHOD}) +public @interface DynamoDBAutoGeneratedTimestamp { + + /** + * The auto-generation strategy; default is {@code ALWAYS}. + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBAutoGenerateStrategy + */ + DynamoDBAutoGenerateStrategy strategy() default DynamoDBAutoGenerateStrategy.ALWAYS; + + /** + * Default generator. + */ + static final class Generator extends DynamoDBAutoGenerator.AbstractGenerator { + private final DynamoDBTypeConverter converter; + + public Generator(Class targetType, DynamoDBAutoGeneratedTimestamp annotation) { + super(annotation.strategy()); + this.converter = StandardTypeConverters.factory().getConverter(targetType, Date.class); + } + + @Override + public final T generate(T currentValue) { + return converter.convert(new Date()); + } + } + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBAutoGenerator.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBAutoGenerator.java new file mode 100644 index 000000000000..5618c975090a --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBAutoGenerator.java @@ -0,0 +1,73 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.annotations.SdkInternalApi; + +/** + * Generator interface for auto-generating attribute values. + * + *

Auto-generation may be controlled by {@link DynamoDBAutoGenerateStrategy}, + + *

{@link DynamoDBAutoGenerateStrategy#CREATE}, instructs to generate when + * creating the item. The mapper, determines an item is new, or overwriting, + * if it's current value is {@code null}. There is a limitiation when performing + * partial updates using either, + * {@link DynamoDBMapperConfig.SaveBehavior#UPDATE_SKIP_NULL_ATTRIBUTES}, or + * {@link DynamoDBMapperConfig.SaveBehavior#APPEND_SET}. A new value will only + * be generated if the mapper is also generating the key.

+ * + *

{@link DynamoDBAutoGenerateStrategy#ALWAYS}, instructs to always generate + * a new value, applied on any save or batch write operation. + * + *

May be used in combination with {@link DynamoDBAutoGenerated}.

+ * + * @param The object's field/property value type. + * + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBAutoGenerated + */ +@SdkPublicApi +public interface DynamoDBAutoGenerator { + + /** + * Gets the auto-generate strategy. + */ + public DynamoDBAutoGenerateStrategy getGenerateStrategy(); + + /** + * Generates a new value given the current value (or null) if applicable. + */ + public T generate(T currentValue); + + /** + * A generator which holds the {@link DynamoDBAutoGenerateStrategy}. + */ + @SdkInternalApi + static abstract class AbstractGenerator implements DynamoDBAutoGenerator { + private final DynamoDBAutoGenerateStrategy strategy; + + protected AbstractGenerator(DynamoDBAutoGenerateStrategy strategy) { + this.strategy = strategy; + } + + @Override + public DynamoDBAutoGenerateStrategy getGenerateStrategy() { + return this.strategy; + } + } + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBConvertedBool.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBConvertedBool.java new file mode 100644 index 000000000000..3d4d00be3400 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBConvertedBool.java @@ -0,0 +1,89 @@ +/* + * 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.mapper.dynamodb; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to convert a {@link Boolean} to the DynamoDB {@code S} type. + * + *
+ * @DynamoDBConvertedBool(DynamoDBConvertedBool.Format.Y_N)
+ * public boolean isTesting()
+ * 
+ * + *

The standard V1 and V2 compatible conversion schemas will, by default, + * serialize booleans using the DynamoDB {@code N} type, with a value of '1' + * representing 'true' and a value of '0' representing 'false'. To force the + * {@code N} conversion in other schemas, + *

+ * @DynamoDBTyped(DynamoDBAttributeType.N)
+ * public boolean isTesting()
+ * 
+ * + *

The standard V2 conversion schema will by default serialize booleans + * natively using the DynamoDB {@code BOOL} type. To force the native + * {@code BOOL} conversion in other schemas, + *

+ * @DynamoDBTyped(DynamoDBAttributeType.BOOL)
+ * public boolean isTesting()
+ * 
+ * + *

May be used as a meta-annotation.

+ */ +@DynamoDB +@DynamoDBTypeConverted(converter=DynamoDBConvertedBool.Converter.class) +@DynamoDBTyped(DynamoDBMapperFieldModel.DynamoDBAttributeType.S) +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +public @interface DynamoDBConvertedBool { + + /** + * Enumeration of the supported format options. + */ + public static enum Format { true_false, T_F, Y_N }; + + /** + * The format type for converting to and from {@link String}. + */ + Format value(); + + /** + * Boolean type converter. + */ + static final class Converter implements DynamoDBTypeConverter { + private final String valueTrue, valueFalse; + + public Converter(Class targetType, DynamoDBConvertedBool annotation) { + this.valueTrue = annotation.value().name().split("_")[0]; + this.valueFalse = annotation.value().name().split("_")[1]; + } + + @Override + public final String convert(final Boolean object) { + return Boolean.TRUE.equals(object) ? valueTrue : valueFalse; + } + + @Override + public final Boolean unconvert(final String object) { + return valueTrue.equals(object) ? Boolean.TRUE : Boolean.FALSE; + } + } + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBDeleteExpression.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBDeleteExpression.java new file mode 100644 index 000000000000..fabac3a84553 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBDeleteExpression.java @@ -0,0 +1,341 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import java.util.HashMap; +import java.util.Map; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.ConditionalOperator; +import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest; +import software.amazon.awssdk.services.dynamodb.model.ExpectedAttributeValue; + +/** + * Enables adding options to a delete operation. + * For example, you may want to delete only if an attribute has a particular value. + * @see DynamoDBMapper#delete(Object, DynamoDBDeleteExpression) + */ +@SdkPublicApi +public final class DynamoDBDeleteExpression { + + /** Optional expected attributes */ + private Map expectedAttributes; + + /** The logical operator on the expected attribute conditions */ + private String conditionalOperator; + + /** + * A condition that must be satisfied in order for a conditional + * DeleteItem to succeed. + */ + private String conditionExpression; + + /** + * One or more substitution variables for simplifying complex + * expressions. + */ + private java.util.Map expressionAttributeNames; + + /** + * One or more values that can be substituted in an expression. + */ + private java.util.Map expressionAttributeValues; + + /** + * Gets the map of attribute names to expected attribute values to check on delete. + * + * @return The map of attribute names to expected attribute value conditions to check on delete + */ + public Map getExpected() { + return expectedAttributes; + } + + /** + * Sets the expected condition to the map of attribute names to expected attribute values given. + * + * @param expectedAttributes + * The map of attribute names to expected attribute value conditions to check on delete + */ + public void setExpected(Map expectedAttributes) { + this.expectedAttributes = expectedAttributes; + } + + /** + * Sets the expected condition to the map of attribute names to expected + * attribute values given and returns a pointer to this object for + * method-chaining. + * + * @param expectedAttributes + * The map of attribute names to expected attribute value + * conditions to check on delete + */ + public DynamoDBDeleteExpression withExpected(Map expectedAttributes) { + setExpected(expectedAttributes); + return this; + } + + /** + * Adds one entry to the expected conditions and returns a pointer to this + * object for method-chaining. + * + * @param attributeName + * The name of the attribute. + * @param expected + * The expected attribute value. + */ + public DynamoDBDeleteExpression withExpectedEntry(String attributeName, ExpectedAttributeValue expected) { + if (expectedAttributes == null) { + expectedAttributes = new HashMap(); + } + expectedAttributes.put(attributeName, expected); + return this; + } + + /** + * Returns the logical operator on the expected attribute conditions of this + * delete operation. + */ + public String getConditionalOperator() { + return conditionalOperator; + } + + /** + * Sets the logical operator on the expected attribute conditions of this + * delete operation. + */ + public void setConditionalOperator(String conditionalOperator) { + this.conditionalOperator = conditionalOperator; + } + + /** + * Sets the logical operator on the expected attribute conditions of this + * delete operation and returns a pointer to this object for + * method-chaining. + */ + public DynamoDBDeleteExpression withConditionalOperator(String conditionalOperator) { + setConditionalOperator(conditionalOperator); + return this; + } + + /** + * Sets the logical operator on the expected attribute conditions of this + * delete operation. + */ + public void setConditionalOperator(ConditionalOperator conditionalOperator) { + setConditionalOperator(conditionalOperator.toString()); + } + + /** + * Sets the logical operator on the expected attribute conditions of this + * delete operation and returns a pointer to this object for + * method-chaining. + */ + public DynamoDBDeleteExpression withConditionalOperator(ConditionalOperator conditionalOperator) { + return withConditionalOperator(conditionalOperator.toString()); + } + + /** + * A condition that must be satisfied in order for a conditional DeleteItem + * to succeed. + * + * @see DeleteItemRequest#getConditionExpression() + */ + public String getConditionExpression() { + return conditionExpression; + } + + /** + * A condition that must be satisfied in order for a conditional DeleteItem + * to succeed. + * + * @see DeleteItemRequest#setConditionExpression() + */ + public void setConditionExpression(String conditionExpression) { + this.conditionExpression = conditionExpression; + } + + /** + * A condition that must be satisfied in order for a conditional DeleteItem + * to succeed. + * + * @return A reference to this updated object so that method calls can be + * chained together. + * + * @see DeleteItemRequest#withConditionExpression(String) + */ + public DynamoDBDeleteExpression withConditionExpression( + String conditionExpression) { + this.conditionExpression = conditionExpression; + return this; + } + + /** + * One or more substitution variables for simplifying complex expressions. + * + * @return One or more substitution variables for simplifying complex + * expressions. + * @see DeleteItemRequest#getExpressionAttributeNames() + */ + public java.util.Map getExpressionAttributeNames() { + + return expressionAttributeNames; + } + + /** + * One or more substitution variables for simplifying complex expressions. + * + * @param expressionAttributeNames + * One or more substitution variables for simplifying complex + * expressions. + * @see DeleteItemRequest#setExpressionAttributeNames(Map) + */ + public void setExpressionAttributeNames( + java.util.Map expressionAttributeNames) { + this.expressionAttributeNames = expressionAttributeNames; + } + + /** + * One or more substitution variables for simplifying complex expressions. + * + * @param expressionAttributeNames + * One or more substitution variables for simplifying complex + * expressions. + * + * @return A reference to this updated object so that method calls can be + * chained together. + * @see DeleteItemRequest#withExpressionAttributeNames(Map) + */ + public DynamoDBDeleteExpression withExpressionAttributeNames( + java.util.Map expressionAttributeNames) { + setExpressionAttributeNames(expressionAttributeNames); + return this; + } + + /** + * One or more substitution variables for simplifying complex expressions. + * The method adds a new key-value pair into ExpressionAttributeNames + * parameter, and returns a reference to this object so that method calls + * can be chained together. + * + * @param key + * The key of the entry to be added into + * ExpressionAttributeNames. + * @param value + * The corresponding value of the entry to be added into + * ExpressionAttributeNames. + * + * @see DeleteItemRequest#addExpressionAttributeNamesEntry(String, String) + */ + public DynamoDBDeleteExpression addExpressionAttributeNamesEntry( + String key, String value) { + if (null == this.expressionAttributeNames) { + this.expressionAttributeNames = new java.util.HashMap(); + } + if (this.expressionAttributeNames.containsKey(key)) + throw new IllegalArgumentException("Duplicated keys (" + + key.toString() + ") are provided."); + this.expressionAttributeNames.put(key, value); + return this; + } + + /** + * Removes all the entries added into ExpressionAttributeNames. + *

+ * Returns a reference to this object so that method calls can be chained + * together. + */ + public DynamoDBDeleteExpression clearExpressionAttributeNamesEntries() { + this.expressionAttributeNames = null; + return this; + } + + /** + * One or more values that can be substituted in an expression. + * + * @return One or more values that can be substituted in an expression. + * @see DeleteItemRequest#getExpressionAttributeValues() + */ + public java.util.Map getExpressionAttributeValues() { + return expressionAttributeValues; + } + + /** + * One or more values that can be substituted in an expression. + * + * @param expressionAttributeValues + * One or more values that can be substituted in an expression. + * + * @see DeleteItemRequest#setExpressionAttributeValues(Map) + */ + public void setExpressionAttributeValues( + java.util.Map expressionAttributeValues) { + this.expressionAttributeValues = expressionAttributeValues; + } + + /** + * One or more values that can be substituted in an expression. + * + * @param expressionAttributeValues + * One or more values that can be substituted in an expression. + * + * @return A reference to this updated object so that method calls can be + * chained together. + * @see DeleteItemRequest#withExpressionAttributeValues(Map) + */ + public DynamoDBDeleteExpression withExpressionAttributeValues( + java.util.Map expressionAttributeValues) { + setExpressionAttributeValues(expressionAttributeValues); + return this; + } + + /** + * One or more values that can be substituted in an expression. The method + * adds a new key-value pair into ExpressionAttributeValues parameter, and + * returns a reference to this object so that method calls can be chained + * together. + * + * @param key + * The key of the entry to be added into + * ExpressionAttributeValues. + * @param value + * The corresponding value of the entry to be added into + * ExpressionAttributeValues. + */ + public DynamoDBDeleteExpression addExpressionAttributeValuesEntry( + String key, AttributeValue value) { + if (null == this.expressionAttributeValues) { + this.expressionAttributeValues = new java.util.HashMap(); + } + if (this.expressionAttributeValues.containsKey(key)) + throw new IllegalArgumentException("Duplicated keys (" + + key.toString() + ") are provided."); + this.expressionAttributeValues.put(key, value); + return this; + } + + /** + * Removes all the entries added into ExpressionAttributeValues. + *

+ * Returns a reference to this object so that method calls can be chained + * together. + */ + public DynamoDBDeleteExpression clearExpressionAttributeValuesEntries() { + this.expressionAttributeValues = null; + return this; + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBDelimited.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBDelimited.java new file mode 100644 index 000000000000..91e4bf8a04be --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBDelimited.java @@ -0,0 +1,202 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.mapper.dynamodb.StandardBeanProperties.Bean; +import software.amazon.awssdk.mapper.dynamodb.StandardBeanProperties.BeanMap; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.regex.Pattern; + +/** + * Annotation to convert an object into a single delimited {@link String} + * attribute. + * + *

+ * @DynamoDBDelimited(
+ *     attributeNames={"areaCode","exchange","subscriber"},
+ *     delimiter='-'
+ * )
+ * public PhoneNumber getPhoneNumber()
+ * 
+ * + *

Where,

+ *
+ * public class PhoneNumber {
+ *     private String areaCode;
+ *     private String exchange;
+ *     private String subscriber;
+ *
+ *     public String getAreaCode() { return areaCode; }
+ *     public void setAreaCode(String areaCode) { this.areaCode = areaCode; }
+ *
+ *     public String getExchange() { return exchange; }
+ *     public void setExchange(String exchange) { this.exchange = exchange; }
+ *
+ *     public String getSubscriber() { return subscriber; }
+ *     public void setSubscriber(String subscriber) { this.subscriber = subscriber; }
+ * }
+ * 
+ * + *

Would write,

+ *
    + *
  • PhoneNumber("206","266","1000") = "206-266-1000"
  • + *
  • PhoneNumber("206",null,"1000") = "206--1000"
  • + *
  • PhoneNumber("206",null,null) = "206--"
  • + *
  • PhoneNumber(null,"266","1000") = "-266-1000"
  • + *
  • PhoneNumber(null,"266",null) = "-266-"
  • + *
  • PhoneNumber(null,null,"1000") = "--1000"
  • + *
  • PhoneNumber(null,null,null) = null
  • + *
  • null = null
  • + *
+ * + * Conversely, reading not fully formatted values from DynamoDB given, + *
    + *
  • "" = empty string not allowed by DDB but would produce empty object
  • + *
  • "--" = PhoneNumber(null,null,null)
  • + *
  • "-----" = PhoneNumber(null,null,null)
  • + *
  • "206" = PhoneNumber("206",null,null)
  • + *
  • "206-266" = PhoneNumber("206","266",null)
  • + *
  • "206-266-1000-1234-5678" = PhoneNumber("206","266","1000")
  • + *
+ * + *

The converter does not protect against values which may also contain the + * delimiter. If more advanced conversion is required, consider implementing, + * a custom {@link DynamoDBTypeConverter}.

+ * + *

New delimited values may always be appended to the string, however, there + * are some risks in distributed systems where, if one system has updated + * delimiting instructions and begins to persist new values, other systems, + * which also persist that same data, would effectively truncate it back to the + * original format.

+ * + *

Auto-generated annotations are not supported on field/property.

+ * + *

TYpe-converted annotations, annotated by {@link DynamoDBTypeConverted}, + * where the output type is {@link String} are supported. + * + *

May be used as a meta-annotation.

+ */ +@DynamoDB +@DynamoDBTypeConverted(converter=DynamoDBDelimited.Converter.class) +@DynamoDBTyped(DynamoDBMapperFieldModel.DynamoDBAttributeType.S) +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +public @interface DynamoDBDelimited { + + /** + * The delimiter for separating attribute values; default is |. + */ + char delimiter() default '|'; + + /** + * The ordered list of attribute/field names. + */ + String[] attributeNames(); + + /** + * Type converter for string delimited attributes. + */ + static final class Converter implements DynamoDBTypeConverter { + private final Field[] fields; + private final Class targetType; + private final String delimiter; + + public Converter(Class targetType, DynamoDBDelimited annotation) { + BeanMap beans = new BeanMap(targetType, true); + + String[] names = annotation.attributeNames(); + if (names.length <= 1) { + throw new DynamoDBMappingException(targetType + + " missing attributeNames in @DynamoDBDelimited; must specify two or more attribute names"); + } + + this.delimiter = String.valueOf(annotation.delimiter()); + this.fields = new Field[names.length]; + this.targetType = targetType; + + for (int i = 0; i < fields.length; i ++) { + if (beans.containsKey(names[i]) == false) { + throw new DynamoDBMappingException(targetType + " does not map %s on model " + names[i]); + } + this.fields[i] = new Field(targetType, beans.get(names[i])); + } + } + + @Override + public final String convert(final T object) { + StringBuilder string = new StringBuilder(); + for (int i = 0; i < fields.length; i++) { + if (i > 0) { + string.append(delimiter); + } + String value = fields[i].get(object); + if (value != null) { + if (value.contains(delimiter)) { + throw new DynamoDBMappingException(String.format( + "%s[%s] field value \"%s\" must not contain delimiter %s", + targetType, fields[i].bean.properties().attributeName(), value, delimiter + )); + } + string.append(value); + } + } + return string.length() < fields.length ? null : string.toString(); + } + + @Override + public final T unconvert(final String string) { + T object = StandardBeanProperties.DeclaringReflect.newInstance(targetType); + String[] values = string.split(Pattern.quote(delimiter)); + for (int i = 0, its = Math.min(fields.length, values.length); i < its; i++) { + fields[i].set(object, values[i]); + } + return object; + } + + private static final class Field { + private final DynamoDBTypeConverter converter; + private final Bean bean; + private Field(final Class type, final Bean bean) { + if (bean.type().typeConverter() == null) { + this.converter = StandardTypeConverters.factory().getConverter(String.class, bean.type().targetType()); + } else { + this.converter = bean.type().typeConverter(); + } + this.bean = bean; + } + private final String get(final T object) { + V value = bean.reflect().get(object); + if (value == null) { + return null; + } + return converter.convert(value); + } + private final void set(final T object, final String string) { + if (!string.isEmpty()) { + V value = converter.unconvert(string); + if (value != null) { + bean.reflect().set(object, value); + } + } + } + } + } + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBDocument.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBDocument.java new file mode 100644 index 000000000000..d79d255101b6 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBDocument.java @@ -0,0 +1,36 @@ +/* + * 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.mapper.dynamodb; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An annotation that marks a class which can be serialized to a DynamoDB + * document or sub-document. Behaves exactly the same as {@link DynamoDBTable}, + * but without requiring you to specify a {@code tableName}. + */ +@DynamoDB +@DynamoDBTyped(DynamoDBMapperFieldModel.DynamoDBAttributeType.M) +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Inherited +public @interface DynamoDBDocument { + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBFlattened.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBFlattened.java new file mode 100644 index 000000000000..0f1296c2b44b --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBFlattened.java @@ -0,0 +1,68 @@ +/* + * 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.mapper.dynamodb; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation for flattening a complex type. + * + *
+ * @DynamoDBFlattened(attributes={
+ *     @DynamoDBAttribute(mappedBy="start", attributeName="effectiveStartDate"),
+ *     @DynamoDBAttribute(mappedBy="end", attributeName="effectiveEndDate")})
+ * public DateRange getEffectiveRange() { return effectiveRange; }
+ * public void setEffectiveRange(DateRange effectiveRange) { this.effectiveRange = effectiveRange; }
+ *
+ * @DynamoDBFlattened(attributes={
+ *     @DynamoDBAttribute(mappedBy="start", attributeName="extensionStartDate"),
+ *     @DynamoDBAttribute(mappedBy="end", attributeName="extensionEndDate")})
+ * public DateRange getExtensionRange() { return extensionRange; }
+ * public void setExtensionRange(DateRange extensionRange) { this.extensionRange = extensionRange; }
+ * 
+ * + *

Where,

+ *
+ * public class DateRange {
+ *     private Date start;
+ *     private Date end;
+ *
+ *     public Date getStart() { return start; }
+ *     public void setStart(Date start) { this.start = start; }
+ *
+ *     public Date getEnd() { return end; }
+ *     public void setEnd(Date end) { this.end = end; }
+ * }
+ * 
+ * + *

Attributes defined within the complex type may also be annotated.

+ * + *

May be used as a meta-annotation.

+ */ +@DynamoDB +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +public @interface DynamoDBFlattened { + + /** + * Indicates the attributes that should be flattened. + */ + DynamoDBAttribute[] attributes() default {}; + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBGeneratedUuid.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBGeneratedUuid.java new file mode 100644 index 000000000000..7cb5c4530747 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBGeneratedUuid.java @@ -0,0 +1,71 @@ +/* + * 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.mapper.dynamodb; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import java.util.UUID; + +/** + * Annotation for auto-generating a {@link UUID}. + * + *
+ * @DynamoDBGeneratedUuid(DynamoDBAutoGenerateStrategy.CREATE)
+ * public UUID getKey()
+ * 
+ * + *

When applied to a key field, only the strategy + * {@link DynamoDBAutoGenerateStrategy#CREATE} is supported.

+ * + *

The short-formed {@link DynamoDBAutoGeneratedKey} may also be used for + * create only.

+ * + *

May be used as a meta-annotation.

+ * + * @see java.util.UUID + */ +@DynamoDB +@DynamoDBAutoGenerated(generator=DynamoDBGeneratedUuid.Generator.class) +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +public @interface DynamoDBGeneratedUuid { + + /** + * The auto-generation strategy. + */ + DynamoDBAutoGenerateStrategy value(); + + /** + * Default generator. + */ + static final class Generator extends DynamoDBAutoGenerator.AbstractGenerator { + private final DynamoDBTypeConverter converter; + + public Generator(Class targetType, DynamoDBGeneratedUuid annotation) { + super(annotation.value()); + this.converter = StandardTypeConverters.factory().getConverter(targetType, UUID.class); + } + + @Override + public final T generate(final T currentValue) { + return converter.convert(UUID.randomUUID()); + } + } + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBHashKey.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBHashKey.java new file mode 100644 index 000000000000..8bf21215d507 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBHashKey.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.mapper.dynamodb; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation for marking a property as the hash key for a modeled class. + * Applied to the getter method or the class field for a hash key property. If + * the annotation is applied directly to the class field, the corresponding + * getter and setter must be declared in the same class. + *

+ * This annotation is required. + */ +@DynamoDB +@DynamoDBKeyed(software.amazon.awssdk.services.dynamodb.model.KeyType.HASH) +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.METHOD}) +public @interface DynamoDBHashKey { + + /** + * Optional parameter when the name of the attribute as stored in DynamoDB + * should differ from the name used by the getter / setter. + */ + String attributeName() default ""; +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBIgnore.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBIgnore.java new file mode 100644 index 000000000000..09aa87c02b9d --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBIgnore.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.mapper.dynamodb; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation for marking a class property as non-modeled. Applied to the getter + * method or the class field for a non-modeled property. If the annotation is + * applied directly to the class field, the corresponding getter and setter must + * be declared in the same class. + *

+ * All getter methods not marked with this annotation are assumed to be modeled + * properties and included in any save() requests. + */ +@DynamoDB +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.METHOD}) +public @interface DynamoDBIgnore { + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBIndexHashKey.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBIndexHashKey.java new file mode 100644 index 000000000000..639dbf303e52 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBIndexHashKey.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.mapper.dynamodb; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation for marking a property in a class as the attribute to be used as + * the hash key for one or more global secondary indexes on a DynamoDB table. + * Applied to the getter method or the class field for the index hash key + * property. If the annotation is applied directly to the class field, the + * corresponding getter and setter must be declared in the same class. + *

+ * This annotation is required if this attribute will be used as index key for + * item queries. + */ +@DynamoDB +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.METHOD}) +public @interface DynamoDBIndexHashKey { + + /** + * Optional parameter when the name of the attribute as stored in DynamoDB + * should differ from the name used by the getter / setter. + */ + String attributeName() default ""; + + /** + * Parameter for the name of the global secondary index. + *

+ * This is required if this attribute is the index key for only one global secondary + * index. + */ + String globalSecondaryIndexName() default ""; + + /** + * Parameter for the names of the global secondary indexes. + * This is required if this attribute is the index key for multiple global secondary + * indexes. + */ + String[] globalSecondaryIndexNames() default {}; + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBIndexRangeKey.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBIndexRangeKey.java new file mode 100644 index 000000000000..e413d689dbf8 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBIndexRangeKey.java @@ -0,0 +1,76 @@ +/* + * 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.mapper.dynamodb; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation for marking a property in a class as the attribute to be used as + * range key for one or more local secondary indexes on a DynamoDB table. + * Applied to the getter method or the class field for the indexed range key + * property. If the annotation is applied directly to the class field, the + * corresponding getter and setter must be declared in the same class. + *

+ * This annotation is required if this attribute will be used as index key for + * item queries. + */ +@DynamoDB +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.METHOD}) +public @interface DynamoDBIndexRangeKey { + + /** + * Optional parameter when the name of the attribute as stored in DynamoDB + * should differ from the name used by the getter / setter. + */ + String attributeName() default ""; + + /** + * Parameter for the name of the local secondary index. + *

+ * This is required if this attribute is the index key for only one local secondary + * index. + */ + String localSecondaryIndexName() default ""; + + /** + * Parameter for the names of the local secondary indexes. + *

+ * This is required if this attribute is the index key for multiple local secondary + * indexes. + */ + String[] localSecondaryIndexNames() default {}; + + /** + * Parameter for the name of the global secondary index. + *

+ * This is required if this attribute is the index key for only one global secondary + * index. + */ + String globalSecondaryIndexName() default ""; + + /** + * Parameter for the names of the global secondary indexes. + *

+ * This is required if this attribute is the index key for multiple global secondary + * indexes. + */ + String[] globalSecondaryIndexNames() default {}; + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBKeyed.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBKeyed.java new file mode 100644 index 000000000000..a46156848029 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBKeyed.java @@ -0,0 +1,48 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.services.dynamodb.model.KeyType; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation for marking a property a key for a modeled class. + * + *

+ * @DynamoDBKeyed(KeyType.HASH)
+ * public UUID getKey()
+ * 
+ * + *

Alternately, the short-formed {@link DynamoDBHashKey}, and + * {@link DynamoDBRangeKey} may be used directly on the field/getter.

+ * + *

May be used as a meta-annotation.

+ */ +@DynamoDB +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +public @interface DynamoDBKeyed { + + /** + * The primary key type; either {@code HASH} or {@code RANGE}. + */ + KeyType value(); + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBMapper.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBMapper.java new file mode 100644 index 000000000000..2b211089b446 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBMapper.java @@ -0,0 +1,2637 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.mapper.dynamodb.internal.DynamoDBMapperModelFactory; +import software.amazon.awssdk.mapper.dynamodb.internal.UpdateExpressionGenerator; +import software.amazon.awssdk.mapper.dynamodb.internal.VersionAttributeConditionExpressionGenerator; +import com.amazonaws.auth.AWSCredentialsProvider; +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.core.exception.SdkException; +import software.amazon.awssdk.core.retry.RetryUtils; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.BatchLoadRetryStrategy; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.BatchWriteRetryStrategy; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.ConsistentReads; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.SaveBehavior; +import software.amazon.awssdk.services.dynamodb.model.AttributeAction; +import com.amazonaws.services.dynamodbv2.model.AttributeDefinition; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.AttributeValueUpdate; +import software.amazon.awssdk.services.dynamodb.model.BatchGetItemRequest; +import software.amazon.awssdk.services.dynamodb.model.BatchGetItemResponse; +import software.amazon.awssdk.services.dynamodb.model.BatchWriteItemRequest; +import software.amazon.awssdk.services.dynamodb.model.BatchWriteItemResponse; +import software.amazon.awssdk.services.dynamodb.model.Condition; +import software.amazon.awssdk.services.dynamodb.model.ConditionCheck; +import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException; +import software.amazon.awssdk.services.dynamodb.model.ConditionalOperator; +import com.amazonaws.services.dynamodbv2.model.CreateTableRequest; +import software.amazon.awssdk.services.dynamodb.model.Delete; +import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest; +import software.amazon.awssdk.services.dynamodb.model.DeleteRequest; +import com.amazonaws.services.dynamodbv2.model.DeleteTableRequest; +import software.amazon.awssdk.services.dynamodb.model.ExpectedAttributeValue; +import software.amazon.awssdk.services.dynamodb.model.Get; +import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; +import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; +import software.amazon.awssdk.services.dynamodb.model.ItemResponse; +import com.amazonaws.services.dynamodbv2.model.KeySchemaElement; +import software.amazon.awssdk.services.dynamodb.model.KeysAndAttributes; +import software.amazon.awssdk.services.dynamodb.model.Put; +import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; +import software.amazon.awssdk.services.dynamodb.model.PutItemResponse; +import software.amazon.awssdk.services.dynamodb.model.PutRequest; +import software.amazon.awssdk.services.dynamodb.model.QueryRequest; +import software.amazon.awssdk.services.dynamodb.model.QueryResponse; +import software.amazon.awssdk.services.dynamodb.model.ReturnValue; +import software.amazon.awssdk.services.dynamodb.model.ReturnValuesOnConditionCheckFailure; +import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType; +import software.amazon.awssdk.services.dynamodb.model.ScanRequest; +import software.amazon.awssdk.services.dynamodb.model.ScanResponse; +import software.amazon.awssdk.services.dynamodb.model.Select; +import software.amazon.awssdk.services.dynamodb.model.TransactGetItem; +import software.amazon.awssdk.services.dynamodb.model.TransactGetItemsRequest; +import software.amazon.awssdk.services.dynamodb.model.TransactGetItemsResponse; +import software.amazon.awssdk.services.dynamodb.model.TransactWriteItem; +import software.amazon.awssdk.services.dynamodb.model.TransactWriteItemsRequest; +import software.amazon.awssdk.services.dynamodb.model.Update; +import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest; +import software.amazon.awssdk.services.dynamodb.model.UpdateItemResponse; +import software.amazon.awssdk.services.dynamodb.model.WriteRequest; +import com.amazonaws.services.s3.model.Region; + +import software.amazon.awssdk.utils.Logger; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import static software.amazon.awssdk.services.dynamodb.model.KeyType.HASH; +import static software.amazon.awssdk.services.dynamodb.model.KeyType.RANGE; +import static software.amazon.awssdk.mapper.dynamodb.TransactionWriteRequest.TransactionWriteOperation; +import static software.amazon.awssdk.mapper.dynamodb.TransactionWriteRequest.TransactionWriteOperationType; + +/** + * Object mapper for domain-object interaction with DynamoDB. + *

+ * To use, define a domain class that represents an item in a DynamoDB table and + * annotate it with the annotations found in the + * software.amazon.awssdk.mapper.dynamodb package. In order to allow the + * mapper to correctly persist the data, each modeled property in the domain + * class should be accessible via getter and setter methods, and each property + * annotation should be either applied to the getter method or the class field. + * A minimal example using getter annotations: + * + *

+ * @DynamoDBTable(tableName = "TestTable")
+ * public class TestClass {
+ *
+ *     private Long key;
+ *     private double rangeKey;
+ *     private Long version;
+ *
+ *     private Set<Integer> integerSetAttribute;
+ *
+ *     @DynamoDBHashKey
+ *     public Long getKey() {
+ *         return key;
+ *     }
+ *
+ *     public void setKey(Long key) {
+ *         this.key = key;
+ *     }
+ *
+ *     @DynamoDBRangeKey
+ *     public double getRangeKey() {
+ *         return rangeKey;
+ *     }
+ *
+ *     public void setRangeKey(double rangeKey) {
+ *         this.rangeKey = rangeKey;
+ *     }
+ *
+ *     @DynamoDBAttribute(attributeName = "integerSetAttribute")
+ *     public Set<Integer> getIntegerAttribute() {
+ *         return integerSetAttribute;
+ *     }
+ *
+ *     public void setIntegerAttribute(Set<Integer> integerAttribute) {
+ *         this.integerSetAttribute = integerAttribute;
+ *     }
+ *
+ *     @DynamoDBVersionAttribute
+ *     public Long getVersion() {
+ *         return version;
+ *     }
+ *
+ *     public void setVersion(Long version) {
+ *         this.version = version;
+ *     }
+ * }
+ * 
+ *

+ * Save instances of annotated classes to DynamoDB, retrieve them, and delete + * them using the {@link DynamoDBMapper} class, as in the following example. + * + *

+ * DynamoDBMapper mapper = new DynamoDBMapper(dynamoDBClient);
+ * Long hashKey = 105L;
+ * double rangeKey = 1.0d;
+ * TestClass obj = mapper.load(TestClass.class, hashKey, rangeKey);
+ * obj.getIntegerAttribute().add(42);
+ * mapper.save(obj);
+ * mapper.delete(obj);
+ * 
+ *

+ * If you don't have your DynamoDB table set up yet, you can use + * {@link DynamoDBMapper#generateCreateTableRequest(Class)} to construct the + * {@link CreateTableRequest} for the table represented by your annotated class. + * + *

+ * AmazonDynamoDB dynamoDBClient = new AmazonDynamoDBClient();
+ * DynamoDBMapper mapper = new DynamoDBMapper(dynamoDBClient);
+ * CreateTableRequest req = mapper.generateCreateTableRequest(TestClass.class);
+ * // Table provision throughput is still required since it cannot be specified in your POJO
+ * req.setProvisionedThroughput(new ProvisionedThroughput(5L, 5L));
+ * // Fire off the CreateTableRequest using the low-level client
+ * dynamoDBClient.createTable(req);
+ * 
+ *

+ * When using the save, load, and delete methods, {@link DynamoDBMapper} will + * throw {@link DynamoDBMappingException}s to indicate that domain classes are + * incorrectly annotated or otherwise incompatible with this class. Service + * exceptions will always be propagated as {@link SdkClientException}, and + * DynamoDB-specific subclasses such as {@link ConditionalCheckFailedException} + * will be used when possible. + *

+ * This class is thread-safe and can be shared between threads. + * + *

+ * Migrating to the AWS SDK for Java v2 + *

+ * The v2 equivalent of this class is + * DynamoDbEnhancedClient + * + * @see DynamoDBTable + * @see DynamoDBHashKey + * @see DynamoDBRangeKey + * @see DynamoDBAutoGeneratedKey + * @see DynamoDBAttribute + * @see DynamoDBVersionAttribute + * @see DynamoDBIgnore + * @see DynamoDBMarshalling + * @see DynamoDBMapperConfig + */ +@SdkPublicApi +public final class DynamoDBMapper extends AbstractDynamoDBMapper { + + private final DynamoDbClient db; + private final DynamoDBMapperModelFactory models; + private final S3Link.Factory s3Links; + + private final AttributeTransformer transformer; + + /** + * The max back off time for batch get. The configuration for batch write + * has been moved to DynamoDBMapperConfig + */ + static final long MAX_BACKOFF_IN_MILLISECONDS = 1000 * 3; + + /** The max number of items allowed in a BatchWrite request */ + static final int MAX_ITEMS_PER_BATCH = 25; + /** + * This retry count is applicable only when every batch get item request + * results in no data retrieved from server and the un processed keys is + * same as request items + */ + static final int BATCH_GET_MAX_RETRY_COUNT_ALL_KEYS = 5; + + private static final Logger log = Logger.loggerFor(DynamoDBMapper.class); + + /** + * Fail fast when trying to create a subclass of the DynamoDBMapper that + * attempts to override one of the old {@code transformAttributes} methods. + */ + private static void failFastOnIncompatibleSubclass(Class clazz) { + while (clazz != DynamoDBMapper.class) { + Class[] classOverride = new Class[] { + Class.class, + Map.class + }; + Class[] nameOverride = new Class[] { + String.class, + String.class, + Map.class + }; + + for (Method method : clazz.getDeclaredMethods()) { + if (method.getName().equals("transformAttributes")) { + Class[] params = method.getParameterTypes(); + if (Arrays.equals(params, classOverride) + || Arrays.equals(params, nameOverride)) { + + throw new IllegalStateException( + "The deprecated transformAttributes method is " + + "no longer supported as of 1.9.0. Use an " + + "AttributeTransformer to inject custom " + + "attribute transformation logic."); + } + } + } + + clazz = clazz.getSuperclass(); + } + } + + + + /** + * Constructs a new mapper with the service object given, using the default + * configuration. + * + * @param dynamoDB + * The service object to use for all service calls. + * @see DynamoDBMapperConfig#DEFAULT + */ + public DynamoDBMapper(final DynamoDbClient dynamoDB) { + this(dynamoDB, DynamoDBMapperConfig.DEFAULT, null, null); + } + + /** + * Constructs a new mapper with the service object and configuration given. + * + * @param dynamoDB + * The service object to use for all service calls. + * @param config + * The default configuration to use for all service calls. It can + * be overridden on a per-operation basis. + */ + public DynamoDBMapper( + final DynamoDbClient dynamoDB, + final DynamoDBMapperConfig config) { + + this(dynamoDB, config, null, null); + } + + /** + * Constructs a new mapper with the service object and S3 client cache + * given, using the default configuration. + * + * @param ddb + * The service object to use for all service calls. + * @param s3CredentialProvider + * The credentials provider for accessing S3. + * Relevant only if {@link S3Link} is involved. + * @see DynamoDBMapperConfig#DEFAULT + */ + public DynamoDBMapper( + final DynamoDbClient ddb, + final AWSCredentialsProvider s3CredentialProvider) { + + this(ddb, DynamoDBMapperConfig.DEFAULT, s3CredentialProvider); + } + + /** + * Constructs a new mapper with the given service object, configuration, + * and transform hook. + * + * @param dynamoDB + * the service object to use for all service calls + * @param config + * the default configuration to use for all service calls. It + * can be overridden on a per-operation basis + * @param transformer + * The custom attribute transformer to invoke when serializing or + * deserializing an object. + */ + public DynamoDBMapper( + final DynamoDbClient dynamoDB, + final DynamoDBMapperConfig config, + final AttributeTransformer transformer) { + + this(dynamoDB, config, transformer, null); + } + + /** + * Constructs a new mapper with the service object, configuration, and S3 + * client cache given. + * + * @param dynamoDB + * The service object to use for all service calls. + * @param config + * The default configuration to use for all service calls. It can + * be overridden on a per-operation basis. + * @param s3CredentialProvider + * The credentials provider for accessing S3. + * Relevant only if {@link S3Link} is involved. + */ + public DynamoDBMapper( + final DynamoDbClient dynamoDB, + final DynamoDBMapperConfig config, + final AWSCredentialsProvider s3CredentialProvider) { + + this(dynamoDB, config, null, validate(s3CredentialProvider)); + } + + /** + * Throws an exception if the given credentials provider is {@code null}. + */ + private static AWSCredentialsProvider validate( + final AWSCredentialsProvider provider) { + if (provider == null) { + throw new IllegalArgumentException( + "s3 credentials provider must not be null"); + } + return provider; + } + + /** + * Constructor with all parameters. + * + * @param dynamoDB + * The service object to use for all service calls. + * @param config + * The default configuration to use for all service calls. It can + * be overridden on a per-operation basis. + * @param transformer + * The custom attribute transformer to invoke when serializing or + * deserializing an object. + * @param s3CredentialProvider + * The credentials provider for accessing S3. + * Relevant only if {@link S3Link} is involved. + */ + public DynamoDBMapper( + final DynamoDbClient dynamoDB, + final DynamoDBMapperConfig config, + final AttributeTransformer transformer, + final AWSCredentialsProvider s3CredentialsProvider) { + super(config); + + failFastOnIncompatibleSubclass(getClass()); + + this.db = dynamoDB; + this.transformer = transformer; + + this.s3Links = S3Link.Factory.of(s3CredentialsProvider); + + this.models = StandardModelFactories.of(this.s3Links); + } + + @Override + public DynamoDBMapperTableModel getTableModel(Class clazz, DynamoDBMapperConfig config) { + return this.models.getTableFactory(config).getTable(clazz); + } + + @Override + public T load(T keyObject, DynamoDBMapperConfig config) { + @SuppressWarnings("unchecked") + Class clazz = (Class) keyObject.getClass(); + + config = mergeConfig(config); + DynamoDBMapperTableModel model = getTableModel(clazz, config); + + String tableName = getTableName(clazz, keyObject, config); + + Map key = model.convertKey(keyObject); + + GetItemRequest rq = GetItemRequest.builder() + .key(key) + .tableName(tableName) + .consistentRead(config.getConsistentReads() == ConsistentReads.CONSISTENT) + .build(); + + GetItemResponse item = db.getItem(rq); + Map itemAttributes = item.item(); + if ( itemAttributes == null || itemAttributes.isEmpty() ) { + return null; + } + + T object = privateMarshallIntoObject( + toParameters(itemAttributes, clazz, tableName, config)); + + return object; + } + + @Override + public T load(Class clazz, Object hashKey, Object rangeKey, DynamoDBMapperConfig config) { + config = mergeConfig(config); + DynamoDBMapperTableModel model = getTableModel(clazz, config); + T keyObject = model.createKey(hashKey, rangeKey); + return load(keyObject, config); + } + + @Override + public T marshallIntoObject(Class clazz, Map itemAttributes, DynamoDBMapperConfig config) { + config = mergeConfig(config); + + String tableName = getTableName(clazz, config); + + return privateMarshallIntoObject( + toParameters(itemAttributes, clazz, tableName, config)); + } + + /** + * The one true implementation of marshallIntoObject. + */ + private T privateMarshallIntoObject( + AttributeTransformer.Parameters parameters) { + + Class clazz = parameters.getModelClass(); + Map values = untransformAttributes(parameters); + + DynamoDBMapperTableModel model = getTableModel(clazz, parameters.getMapperConfig()); + return model.unconvert(values); + } + + @Override + public List marshallIntoObjects(Class clazz, List> itemAttributes, DynamoDBMapperConfig config) { + config = mergeConfig(config); + + List result = new ArrayList(itemAttributes.size()); + for (Map item : itemAttributes) { + result.add(marshallIntoObject(clazz, item)); + } + return result; + } + + /** + * A replacement for {@link #marshallIntoObjects(Class, List)} that takes + * an extra set of parameters to be tunneled through to + * {@code privateMarshalIntoObject} (if nothing along the way is + * overridden). It's package-private because some of the Paginated*List + * classes call back into it, but final because no one, even in this + * package, should ever override it. + */ + final List marshallIntoObjects( + final List> parameters + ) { + List result = new ArrayList(parameters.size()); + + for (AttributeTransformer.Parameters entry : parameters) { + result.add(privateMarshallIntoObject(entry)); + } + + return result; + } + + @Override + public void save(T object, + DynamoDBSaveExpression saveExpression, + final DynamoDBMapperConfig config) { + DynamoDBMapperConfig finalConfig = mergeConfig(config); + + @SuppressWarnings("unchecked") + Class clazz = (Class) object.getClass(); + String tableName = getTableName(clazz, object, finalConfig); + + DynamoDBMapperTableModel model = getTableModel(clazz, finalConfig); + + /* + * We use a putItem request instead of updateItem request either when + * CLOBBER or PUT is configured, or part of the primary key of the object needs + * to be auto-generated. + */ + boolean usePut = (finalConfig.getSaveBehavior() == SaveBehavior.CLOBBER + || finalConfig.getSaveBehavior() == SaveBehavior.PUT) + || anyKeyGeneratable(model, object, finalConfig.getSaveBehavior()); + + SaveObjectHandler saveObjectHandler; + + if (usePut) { + saveObjectHandler = this.new SaveObjectHandler(clazz, object, + tableName, finalConfig, saveExpression) { + + @Override + protected void onPrimaryKeyAttributeValue(String attributeName, + AttributeValue keyAttributeValue) { + /* Treat key values as common attribute value updates. */ + getAttributeValueUpdates().put(attributeName, + AttributeValueUpdate.builder().value(keyAttributeValue) + .action("PUT").build()); + } + + /* Use default implementation of onNonKeyAttribute(...) */ + + @Override + protected void onNullNonKeyAttribute(String attributeName) { + /* When doing a force put, we can safely ignore the null-valued attributes. */ + return; + } + + @Override + protected void executeLowLevelRequest() { + /* Send a putItem request */ + doPutItem(); + } + }; + } else { + saveObjectHandler = this.new SaveObjectHandler(clazz, object, + tableName, finalConfig, saveExpression) { + + @Override + protected void onPrimaryKeyAttributeValue(String attributeName, + AttributeValue keyAttributeValue) { + /* Put it in the key collection which is later used in the updateItem request. */ + getPrimaryKeyAttributeValues().put(attributeName, keyAttributeValue); + } + + + @Override + protected void onNonKeyAttribute(String attributeName, + AttributeValue currentValue) { + /* If it's a set attribute and the mapper is configured with APPEND_SET, + * we do an "ADD" update instead of the default "PUT". + */ + if (getLocalSaveBehavior() == SaveBehavior.APPEND_SET) { + if (currentValue.hasBs() + || currentValue.hasNs() + || currentValue.hasSs()) { + getAttributeValueUpdates().put( + attributeName, + AttributeValueUpdate.builder().value( + currentValue).action("ADD").build()); + return; + } + } + /* Otherwise, we do the default "PUT" update. */ + super.onNonKeyAttribute(attributeName, currentValue); + } + + @Override + protected void onNullNonKeyAttribute(String attributeName) { + /* + * If UPDATE_SKIP_NULL_ATTRIBUTES or APPEND_SET is + * configured, we don't delete null value attributes. + */ + if (getLocalSaveBehavior() == SaveBehavior.UPDATE_SKIP_NULL_ATTRIBUTES + || getLocalSaveBehavior() == SaveBehavior.APPEND_SET) { + return; + } + + else { + /* Delete attributes that are set as null in the object. */ + getAttributeValueUpdates() + .put(attributeName, + AttributeValueUpdate.builder() + .action("DELETE").build()); + } + } + + @Override + protected void executeLowLevelRequest() { + UpdateItemResponse updateItemResult = doUpdateItem(); + + // The UpdateItem request is specified to return ALL_NEW + // attributes of the affected item. So if the returned + // UpdateItemResponse does not include any ReturnedAttributes, + // it indicates the UpdateItem failed silently (e.g. the + // key-only-put nightmare - + // https://forums.aws.amazon.com/thread.jspa?threadID=86798&tstart=25), + // in which case we should re-send a PutItem + // request instead. + if (!updateItemResult.hasAttributes() + || updateItemResult.attributes().isEmpty()) { + // Before we proceed with PutItem, we need to put all + // the key attributes (prepared for the + // UpdateItemRequest) into the AttributeValueUpdates + // collection. + for (String keyAttributeName : getPrimaryKeyAttributeValues().keySet()) { + getAttributeValueUpdates().put(keyAttributeName, + AttributeValueUpdate.builder() + .value(getPrimaryKeyAttributeValues().get(keyAttributeName)) + .action("PUT").build()); + } + + doPutItem(); + } + } + }; + } + + saveObjectHandler.execute(); + } + + /** + * The handler for saving object using DynamoDBMapper. Caller should + * implement the abstract methods to provide the expected behavior on each + * scenario, and this handler will take care of all the other basic workflow + * and common operations. + */ + protected abstract class SaveObjectHandler { + + protected final Object object; + protected final Class clazz; + private final String tableName; + private final DynamoDBMapperConfig saveConfig; + + private final Map primaryKeys; + private final Map updateValues; + + /** + * Any expected value conditions specified by the implementation of + * DynamoDBMapper, e.g. value assertions on versioned attributes. + */ + private final Map internalExpectedValueAssertions; + + /** + * Additional expected value conditions specified by the user. + */ + protected final Map userProvidedExpectedValueConditions; + + /** + * Condition operator on the additional expected value conditions + * specified by the user. + */ + protected final String userProvidedConditionOperator; + + private final List inMemoryUpdates; + + /** + * Constructs a handler for saving the specified model object. + * + * @param object The model object to be saved. + * @param clazz The domain class of the object. + * @param tableName The table name. + * @param saveConifg The mapper configuration used for this save. + * @param saveExpression The save expression, including the user-provided conditions and an optional logic operator. + */ + public SaveObjectHandler( + Class clazz, + Object object, + String tableName, + DynamoDBMapperConfig saveConfig, + DynamoDBSaveExpression saveExpression) { + + this.clazz = clazz; + this.object = object; + this.tableName = tableName; + this.saveConfig = saveConfig; + + if (saveExpression != null) { + userProvidedExpectedValueConditions = saveExpression + .getExpected(); + userProvidedConditionOperator = saveExpression + .getConditionalOperator(); + } else { + userProvidedExpectedValueConditions = null; + userProvidedConditionOperator = null; + } + + updateValues = new HashMap(); + internalExpectedValueAssertions = new HashMap(); + inMemoryUpdates = new LinkedList(); + primaryKeys = new HashMap(); + } + + /** + * The general workflow of a save operation. + */ + public void execute() { + DynamoDBMapperTableModel model = getTableModel((Class)clazz, saveConfig); + for ( DynamoDBMapperFieldModel field : model.fields() ) { + if ( canGenerate(model, object, getLocalSaveBehavior(), field) ) { + if ( field.keyType() != null || field.indexed() ) { + onAutoGenerateAssignableKey(field); + } else if ( field.versioned() ) { + onVersionAttribute(field); + } else { + onAutoGenerate(field); + } + } else if ( field.keyType() != null ) { + AttributeValue newAttributeValue = field.convert(field.get(object)); + if ( newAttributeValue == null ) { + throw new DynamoDBMappingException( + clazz.getSimpleName() + "[" + field.name() + "]; null or empty value for primary key" + ); + } + onPrimaryKeyAttributeValue(field.name(), newAttributeValue); + } else { + AttributeValue currentValue = field.convert(field.get(object)); + if ( currentValue != null ) { + onNonKeyAttribute(field.name(), currentValue); + } else { + onNullNonKeyAttribute(field.name()); + } + } + } + + /* + * Execute the implementation of the low level request. + */ + executeLowLevelRequest(); + + /* + * Finally, after the service call has succeeded, update the + * in-memory object with new field values as appropriate. This + * currently takes into account of auto-generated keys and versioned + * attributes. + */ + for ( ValueUpdate update : inMemoryUpdates ) { + update.apply(); + } + } + + /** + * Implement this method to do the necessary operations when a primary key + * attribute is set with some value. + * + * @param attributeName + * The name of the primary key attribute. + * @param keyAttributeValue + * The AttributeValue of the primary key attribute as specified in + * the object. + */ + protected abstract void onPrimaryKeyAttributeValue(String attributeName, AttributeValue keyAttributeValue); + + /** + * Implement this method for necessary operations when a non-key + * attribute is set a non-null value in the object. + * The default implementation simply adds a "PUT" update for the given attribute. + * + * @param attributeName + * The name of the non-key attribute. + * @param currentValue + * The updated value of the given attribute. + */ + protected void onNonKeyAttribute(String attributeName, AttributeValue currentValue) { + updateValues.put(attributeName, AttributeValueUpdate.builder() + .value(currentValue).action("PUT").build()); + } + + /** + * Implement this method for necessary operations when a non-key + * attribute is set null in the object. + * + * @param attributeName + * The name of the non-key attribute. + */ + protected abstract void onNullNonKeyAttribute(String attributeName); + + /** + * Implement this method to send the low-level request that is necessary + * to complete the save operation. + */ + protected abstract void executeLowLevelRequest(); + + /** Get the SaveBehavior used locally for this save operation. **/ + protected SaveBehavior getLocalSaveBehavior() { + return saveConfig.getSaveBehavior(); + } + + /** Get the table name **/ + protected String getTableName() { + return tableName; + } + + /** Get the map of all the specified primamry keys of the saved object. **/ + protected Map getPrimaryKeyAttributeValues() { + return primaryKeys; + } + + /** Get the map of AttributeValueUpdate on each modeled attribute. **/ + protected Map getAttributeValueUpdates() { + return updateValues; + } + + /** + * Merge and return all the expected value conditions (either + * user-specified or imposed by the internal implementation of + * DynamoDBMapper) for this save operation. + */ + protected Map mergeExpectedAttributeValueConditions() { + return DynamoDBMapper.mergeExpectedAttributeValueConditions( + internalExpectedValueAssertions, + userProvidedExpectedValueConditions, + userProvidedConditionOperator); + } + + /** Get the list of all the necessary in-memory update on the object. **/ + protected List getInMemoryUpdates() { + return inMemoryUpdates; + } + + /** + * Save the item using a UpdateItem request. The handler will call this + * method if + *
    + *
  • CLOBBER configuration is not being used; + *
  • AND the item does not contain auto-generated key value; + *
+ *

+ * The ReturnedValues parameter for the UpdateItem request is set as + * ALL_NEW, which means the service should return all of the attributes + * of the new version of the item after the update. The handler will use + * the returned attributes to detect silent failure on the server-side. + */ + protected UpdateItemResponse doUpdateItem() { + UpdateItemRequest req = UpdateItemRequest.builder() + .tableName(getTableName()) + .key(getPrimaryKeyAttributeValues()) + .attributeUpdates( + transformAttributeUpdates( + this.clazz, + getTableName(), + getPrimaryKeyAttributeValues(), + getAttributeValueUpdates(), + saveConfig)) + .expected(mergeExpectedAttributeValueConditions()) + .conditionalOperator(userProvidedConditionOperator) + .returnValues(ReturnValue.ALL_NEW) + .build(); + + return db.updateItem(req); + } + + /** + * Save the item using a PutItem request. The handler will call this + * method if + *

    + *
  • CLOBBER configuration is being used; + *
  • OR the item contains auto-generated key value; + *
  • OR an UpdateItem request has silently failed (200 response with + * no affected attribute), which indicates the key-only-put scenario + * that we used to handle by the keyOnlyPut(...) hack. + *
+ */ + protected PutItemResponse doPutItem() { + Map attributeValues = convertToItem(getAttributeValueUpdates()); + + attributeValues = transformAttributes( + toParameters(attributeValues, + this.clazz, + getTableName(), + saveConfig)); + PutItemRequest req = PutItemRequest.builder() + .tableName(getTableName()) + .item(attributeValues) + .expected(mergeExpectedAttributeValueConditions()) + .conditionalOperator(userProvidedConditionOperator) + .build(); + + return db.putItem(req); + } + + /** + * Auto-generates the attribute value. + * @param mapping The mapping details. + */ + private void onAutoGenerate(DynamoDBMapperFieldModel field) { + AttributeValue value = field.convert(field.generate(field.get(object))); + updateValues.put(field.name(), AttributeValueUpdate.builder().action("PUT").value(value).build()); + inMemoryUpdates.add(new ValueUpdate(field, value, object)); + } + + /** + * Auto-generates the key. + */ + private void onAutoGenerateAssignableKey(DynamoDBMapperFieldModel field) { + // Generate the new key value first, then ensure it doesn't exist. + onAutoGenerate(field); + + if (getLocalSaveBehavior() != SaveBehavior.CLOBBER + && !internalExpectedValueAssertions.containsKey(field.name()) + && field.getGenerateStrategy() != DynamoDBAutoGenerateStrategy.ALWAYS) { + // Add an expect clause to make sure that the item + // doesn't already exist, since it's supposed to be new + internalExpectedValueAssertions.put(field.name(), + ExpectedAttributeValue.builder().exists(false).build()); + } + } + + /** + * Auto-generates the version. + * @param mapping The mapping details. + */ + private void onVersionAttribute(DynamoDBMapperFieldModel field) { + if ( getLocalSaveBehavior() != SaveBehavior.CLOBBER + && !internalExpectedValueAssertions.containsKey(field.name())) { + // First establish the expected (current) value for the + // update call + // For new objects, insist that the value doesn't exist. + // For existing ones, insist it has the old value. + Object current = field.get(object); + if (current == null) { + internalExpectedValueAssertions.put(field.name(), + ExpectedAttributeValue.builder().exists(false).build()); + } else { + internalExpectedValueAssertions.put(field.name(), + ExpectedAttributeValue.builder().exists(true).value(field.convert(current)).build()); + } + } + + // Generate the new version value + onAutoGenerate(field); + } + + /** + * Converts the {@link AttributeValueUpdate} map given to an equivalent + * {@link AttributeValue} map. + */ + private Map convertToItem(Map putValues) { + Map map = new HashMap(); + for ( Entry entry : putValues.entrySet() ) { + String attributeName = entry.getKey(); + AttributeValue attributeValue = entry.getValue().value(); + String attributeAction = entry.getValue().actionAsString(); + + /* + * AttributeValueUpdate allows nulls for its values, since they are + * semantically meaningful. AttributeValues never have null values. + */ + if ( attributeValue != null + && !AttributeAction.DELETE.toString().equals(attributeAction)) { + map.put(attributeName, attributeValue); + } + } + return map; + } + + private Map transformAttributeUpdates( + final Class clazz, + final String tableName, + final Map keys, + final Map updateValues, + final DynamoDBMapperConfig config + ) { + Map item = convertToItem(updateValues); + + HashSet keysAdded = new HashSet(); + for (Map.Entry e : keys.entrySet()) { + if (!item.containsKey(e.getKey())) { + keysAdded.add(e.getKey()); + item.put(e.getKey(), e.getValue()); + } + } + + AttributeTransformer.Parameters parameters = + toParameters(item, true, clazz, tableName, config); + + String hashKey = parameters.getHashKeyName(); + + if (!item.containsKey(hashKey)) { + item.put(hashKey, keys.get(hashKey)); + } + + item = transformAttributes(parameters); + + for(Map.Entry entry: item.entrySet()) { + if (keysAdded.contains(entry.getKey())) { + // This was added in for context before calling + // transformAttributes, but isn't actually being changed. + continue; + } + + AttributeValueUpdate update = updateValues.get(entry.getKey()); + if (update != null) { + updateValues.put(entry.getKey(), + update.toBuilder().value(entry.getValue()).build()); + } else { + updateValues.put(entry.getKey(), + AttributeValueUpdate.builder() + .value(entry.getValue()) + .action("PUT") + .build()); + } + } + + return updateValues; + } + } + + @Override + public void delete(T object, DynamoDBDeleteExpression deleteExpression, DynamoDBMapperConfig config) { + config = mergeConfig(config); + + @SuppressWarnings("unchecked") + Class clazz = (Class) object.getClass(); + DynamoDBMapperTableModel model = getTableModel(clazz, config); + + String tableName = getTableName(clazz, object, config); + + Map key = model.convertKey(object); + + /* + * If there is a version field, make sure we assert its value. If the + * version field is null (only should happen in unusual circumstances), + * pretend it doesn't have a version field after all. + */ + Map internalAssertions = new HashMap(); + if ( config.getSaveBehavior() != SaveBehavior.CLOBBER && model.versioned() ) { + for ( DynamoDBMapperFieldModel field : model.versions() ) { + AttributeValue current = field.getAndConvert(object); + if (current == null) { + internalAssertions.put(field.name(), ExpectedAttributeValue.builder().exists(false).build()); + } else { + internalAssertions.put(field.name(), ExpectedAttributeValue.builder().exists(true).value(current).build()); + } + break; + } + } + + DeleteItemRequest.Builder reqBuilder = DeleteItemRequest.builder() + .key(key) + .tableName(tableName) + .expected(internalAssertions); + + if (deleteExpression != null) { + String conditionalExpression = deleteExpression.getConditionExpression(); + + if (conditionalExpression != null) { + if (internalAssertions != null && !internalAssertions.isEmpty()) { + throw SdkClientException.create( + "Condition Expressions cannot be used if a versioned attribute is present"); + } + + reqBuilder = reqBuilder + .conditionExpression(conditionalExpression) + .expressionAttributeNames( + deleteExpression.getExpressionAttributeNames()) + .expressionAttributeValues( + deleteExpression.getExpressionAttributeValues()); + } + + reqBuilder = reqBuilder.expected( + mergeExpectedAttributeValueConditions(internalAssertions, + deleteExpression.getExpected(), + deleteExpression.getConditionalOperator())) + .conditionalOperator( + deleteExpression.getConditionalOperator()); + + } + db.deleteItem(reqBuilder.build()); + } + + @Override + public void transactionWrite(TransactionWriteRequest transactionWriteRequest, DynamoDBMapperConfig config) { + if (transactionWriteRequest == null || isNullOrEmpty(transactionWriteRequest.getTransactionWriteOperations())) { + throw SdkClientException.create("Input request is null or empty"); + } + + DynamoDBMapperConfig finalConfig = mergeConfig(config); + + List writeOperations = transactionWriteRequest.getTransactionWriteOperations(); + List inMemoryUpdates = new LinkedList(); + List transactWriteItems = new ArrayList(); + + for (TransactionWriteOperation writeOperation : writeOperations) { + transactWriteItems.add(generateTransactWriteItem(writeOperation, + inMemoryUpdates, + finalConfig)); + + } + + TransactWriteItemsRequest transactWriteItemsRequest = TransactWriteItemsRequest.builder() + .clientRequestToken(transactionWriteRequest.getIdempotencyToken()) + .transactItems(transactWriteItems) + .build(); + + db.transactWriteItems(transactWriteItemsRequest); + + // Update the inMemory values for autogenerated attributeValues after successful completion of transaction + for (ValueUpdate update : inMemoryUpdates) { + update.apply(); + } + } + + @Override + public List transactionLoad(TransactionLoadRequest transactionLoadRequest, DynamoDBMapperConfig config) { + + if (transactionLoadRequest == null || isNullOrEmpty(transactionLoadRequest.getObjectsToLoad())) { + return new ArrayList(); + } + + DynamoDBMapperConfig finalConfig = mergeConfig(config); + + List objectsToLoad = transactionLoadRequest.getObjectsToLoad(); + List transactionLoadExpressions = transactionLoadRequest.getObjectLoadExpressions(); + List transactGetItems = new ArrayList(); + List> classList = new ArrayList>(); + List tableNameList = new ArrayList(); + for (int i = 0 ; i < objectsToLoad.size() ; i++) { + Object objectToLoad = objectsToLoad.get(i); + DynamoDBTransactionLoadExpression expressionForLoad = transactionLoadExpressions.get(i); + Class clazz = (Class)objectToLoad.getClass(); + + String tableName = getTableName(clazz, objectToLoad, finalConfig); + tableNameList.add(tableName); + classList.add(clazz); + DynamoDBMapperTableModel model = getTableModel(clazz, finalConfig); + + Map key = model.convertKey(objectToLoad); + Get.Builder getItem = Get.builder() + .tableName(tableName) + .key(key); + if (expressionForLoad != null) { + getItem.expressionAttributeNames(expressionForLoad.getExpressionAttributeNames()); + getItem.projectionExpression(expressionForLoad.getProjectionExpression()); + } + transactGetItems.add(TransactGetItem.builder().get(getItem.build()).build()); + } + + TransactGetItemsRequest transactGetItemsRequest = TransactGetItemsRequest.builder() + .transactItems(transactGetItems) + .build(); + TransactGetItemsResponse transactGetItemsResult = db.transactGetItems(transactGetItemsRequest); + List responseItems = transactGetItemsResult.responses(); + + List resultObjects = new ArrayList(); + + for (int i = 0 ; i < responseItems.size(); i++) { + if (!responseItems.get(i).hasItem()) { + resultObjects.add(null); + } else { + resultObjects.add( + privateMarshallIntoObject( + toParameters(responseItems.get(i).item(), + classList.get(i), + tableNameList.get(i), + finalConfig))); + } + } + + return resultObjects; + } + + @Override + public List batchWrite(Iterable objectsToWrite, + Iterable objectsToDelete, + DynamoDBMapperConfig config) { + config = mergeConfig(config); + + List totalFailedBatches = new LinkedList(); + + StringListMap requestItems = new StringListMap(); + + List inMemoryUpdates = new LinkedList(); + for ( Object toWrite : objectsToWrite ) { + Class clazz = (Class)toWrite.getClass(); + String tableName = getTableName(clazz, toWrite, config); + + Map attributeValues = new HashMap(); + + // Look at every getter and construct a value object for it + DynamoDBMapperTableModel model = getTableModel(clazz, config); + for ( DynamoDBMapperFieldModel field : model.fields() ) { + AttributeValue currentValue = null; + if ( canGenerate(model, toWrite, config.getSaveBehavior(), field) && !field.versioned() ) { + currentValue = field.convert(field.generate(field.get(toWrite))); + inMemoryUpdates.add(new ValueUpdate(field, currentValue, toWrite)); + } else { + currentValue = field.convert(field.get(toWrite)); + } + if ( currentValue != null ) { + attributeValues.put(field.name(), currentValue); + } + } + + if ( !requestItems.containsKey(tableName) ) { + requestItems.put(tableName, new LinkedList()); + } + + AttributeTransformer.Parameters parameters = + toParameters(attributeValues, clazz, tableName, config); + + requestItems.add(tableName, WriteRequest.builder() + .putRequest(PutRequest.builder().item(transformAttributes(parameters)).build()) + .build()); + } + + for ( Object toDelete : objectsToDelete ) { + Class clazz = (Class)toDelete.getClass(); + + String tableName = getTableName(clazz, toDelete, config); + DynamoDBMapperTableModel model = getTableModel(clazz, config); + + Map key = model.convertKey(toDelete); + + requestItems.add(tableName, WriteRequest.builder() + .deleteRequest(DeleteRequest.builder().key(key).build()) + .build()); + } + + // Break into chunks of 25 items and make service requests to DynamoDB + for (StringListMap batch : requestItems.subMaps(MAX_ITEMS_PER_BATCH, true)) { + List failedBatches = writeOneBatch(batch, config.getBatchWriteRetryStrategy()); + if (failedBatches != null) { + totalFailedBatches.addAll(failedBatches); + + // If contains throttling exception, we do a backoff + if (containsThrottlingException(failedBatches)) { + pause(config.getBatchWriteRetryStrategy().getDelayBeforeRetryUnprocessedItems( + Collections.unmodifiableMap(batch), 0)); + } + } + } + + // Once the entire batch is processed, update assigned keys in memory + for ( ValueUpdate update : inMemoryUpdates ) { + update.apply(); + } + + return totalFailedBatches; + } + + /** + * Process one batch of requests(max 25). It will divide the batch if + * receives request too large exception(the total size of the request is beyond 1M). + */ + private List writeOneBatch( + StringListMap batch, + BatchWriteRetryStrategy batchWriteRetryStrategy) { + + List failedBatches = new LinkedList(); + FailedBatch failedBatch = doBatchWriteItemWithRetry(batch, batchWriteRetryStrategy); + + if (failedBatch != null) { + // If the exception is request entity too large, we divide the batch + // into smaller parts. + + if (failedBatch.isRequestEntityTooLarge()) { + + // If only one item left, the item size must beyond 64k, which + // exceedes the limit. + + if (failedBatch.size() == 1) { + failedBatches.add(failedBatch); + } else { + for (StringListMap subBatch : batch.subMaps(2, false)) { + failedBatches.addAll(writeOneBatch(subBatch, batchWriteRetryStrategy)); + } + } + + } else { + failedBatches.add(failedBatch); + } + + } + return failedBatches; + } + + /** + * Check whether there are throttling exception in the failed batches. + */ + private boolean containsThrottlingException (List failedBatches) { + for (FailedBatch failedBatch : failedBatches) { + if (failedBatch.isThrottling()) { + return true; + } + } + return false; + } + + /** + * Continue trying to process the batch and retry on UnproccessedItems as + * according to the specified BatchWriteRetryStrategy + */ + private FailedBatch doBatchWriteItemWithRetry( + Map> batch, + BatchWriteRetryStrategy batchWriteRetryStrategy) { + + BatchWriteItemResponse result = null; + int retries = 0; + int maxRetries = batchWriteRetryStrategy + .getMaxRetryOnUnprocessedItems(Collections + .unmodifiableMap(batch)); + + FailedBatch failedBatch = null; + Map> pendingItems = batch; + + while (true) { + try { + result = db.batchWriteItem(BatchWriteItemRequest.builder().requestItems(pendingItems).build()); + } catch (Exception e) { + failedBatch = new FailedBatch(); + failedBatch.setUnprocessedItems(pendingItems); + failedBatch.setException(e); + return failedBatch; + } + pendingItems = result.unprocessedItems(); + + if (pendingItems.size() > 0) { + + // return pendingItems as a FailedBatch if we have exceeded max retry + if (maxRetries >= 0 && retries >= maxRetries) { + failedBatch = new FailedBatch(); + failedBatch.setUnprocessedItems(pendingItems); + failedBatch.setException(null); + return failedBatch; + } + + pause(batchWriteRetryStrategy.getDelayBeforeRetryUnprocessedItems( + Collections.unmodifiableMap(pendingItems), retries)); + retries++; + } else { + break; + } + } + return failedBatch; + } + + @Override + public Map> batchLoad(Iterable itemsToGet, DynamoDBMapperConfig config) { + config = mergeConfig(config); + boolean consistentReads = (config.getConsistentReads() == ConsistentReads.CONSISTENT); + + if (itemsToGet == null) { + return new HashMap>(); + } + + Map>> keysByTableName = + new HashMap>>(); + Map> classesByTableName = new HashMap>(); + Map> resultSet = new HashMap>(); + int count = 0; + + for ( Object keyObject : itemsToGet ) { + Class clazz = (Class)keyObject.getClass(); + DynamoDBMapperTableModel model = getTableModel(clazz, config); + + String tableName = getTableName(clazz, keyObject, config); + classesByTableName.put(tableName, clazz); + + if ( !keysByTableName.containsKey(tableName) ) { + keysByTableName.put(tableName, new LinkedList>()); + } + + keysByTableName.get(tableName).add(model.convertKey(keyObject)); + + // Reach the maximum number which can be handled in a single batchGet + if ( ++count == 100 ) { + processBatchGetRequest(classesByTableName, buildKeysAndAttributes(keysByTableName, consistentReads), + resultSet, config); + keysByTableName.clear(); + count = 0; + } + } + + if ( count > 0 ) { + processBatchGetRequest(classesByTableName, buildKeysAndAttributes(keysByTableName, consistentReads), + resultSet, config); + } + + return resultSet; + } + + private static Map buildKeysAndAttributes( + Map>> keysByTableName, boolean consistentReads) { + Map requestItems = new HashMap(); + for (Map.Entry>> entry : keysByTableName.entrySet()) { + requestItems.put(entry.getKey(), KeysAndAttributes.builder() + .consistentRead(consistentReads) + .keys(entry.getValue()) + .build()); + } + return requestItems; + } + + @Override + public Map> batchLoad(Map, List> itemsToGet, DynamoDBMapperConfig config) { + config = mergeConfig(config); + List keys = new ArrayList(); + if ( itemsToGet != null ) { + for ( Class clazz : itemsToGet.keySet() ) { + if ( itemsToGet.get(clazz) != null ) { + DynamoDBMapperTableModel model = getTableModel(clazz, config); + for ( KeyPair keyPair : itemsToGet.get(clazz) ) { + keys.add(model.createKey(keyPair.getHashKey(), keyPair.getRangeKey())); + } + } + } + } + return batchLoad(keys, config); + } + + /** + * @param config never null + */ + private void processBatchGetRequest( + final Map> classesByTableName, + final Map requestItems, + final Map> resultSet, + final DynamoDBMapperConfig config) { + + BatchGetItemResponse batchGetItemResult = null; + BatchGetItemRequest batchGetItemRequest = BatchGetItemRequest.builder() + .requestItems(requestItems) + .build(); + + BatchLoadRetryStrategy batchLoadStrategy = config.getBatchLoadRetryStrategy(); + + BatchLoadContext batchLoadContext = new BatchLoadContext(batchGetItemRequest); + + int retries = 0; + + do { + if ( batchGetItemResult != null ) { + retries++; + batchLoadContext.setRetriesAttempted(retries); + if (!isNullOrEmpty(batchGetItemResult.unprocessedKeys())) { + pause(batchLoadStrategy.getDelayBeforeNextRetry(batchLoadContext)); + batchGetItemRequest = batchGetItemRequest.toBuilder() + .requestItems(batchGetItemResult.unprocessedKeys()) + .build(); + batchLoadContext.setBatchGetItemRequest(batchGetItemRequest); + } + } + + batchGetItemResult = db.batchGetItem(batchGetItemRequest); + + Map>> responses = batchGetItemResult.responses(); + for ( String tableName : responses.keySet() ) { + List objects = null; + if ( resultSet.get(tableName) != null ) { + objects = resultSet.get(tableName); + } else { + objects = new LinkedList(); + } + + Class clazz = classesByTableName.get(tableName); + + for ( Map item : responses.get(tableName) ) { + AttributeTransformer.Parameters parameters = + toParameters(item, clazz, tableName, config); + objects.add(privateMarshallIntoObject(parameters)); + } + + resultSet.put(tableName, objects); + } + + batchLoadContext.setBatchGetItemResult(batchGetItemResult); + + // the number of unprocessed keys and Batch Load Strategy will drive the number of retries + } while ( batchLoadStrategy.shouldRetry(batchLoadContext) ); + + if (!isNullOrEmpty(batchGetItemResult.unprocessedKeys())) { + throw new BatchGetItemException( + "The BatchGetItemResult has unprocessed keys after max retry attempts. Catch the BatchGetItemException to get the list of unprocessed keys.", + batchGetItemResult.unprocessedKeys(), resultSet); + } + } + + private static boolean isNullOrEmpty(Map map) { + return map == null || map.isEmpty(); + } + + private static boolean isNullOrEmpty(List list) { + return list == null || list.isEmpty(); + } + + /** Normalizes v2's empty (non-null) final-page LastEvaluatedKey back to null per the ResultPage contract. */ + private static Map lastEvaluatedKeyOrNull(boolean hasLastEvaluatedKey, + Map lastEvaluatedKey) { + return hasLastEvaluatedKey && !lastEvaluatedKey.isEmpty() ? lastEvaluatedKey : null; + } + + /** + * Determnes if any of the primary keys require auto-generation. + */ + private static boolean anyKeyGeneratable( + final DynamoDBMapperTableModel model, + final T object, + final SaveBehavior saveBehavior + ) { + for (DynamoDBMapperFieldModel field : model.keys()) { + if (canGenerate(model, object, saveBehavior, field)) { + return true; + } + } + return false; + } + + /** + * Determines if the mapping value can be auto-generated. + */ + private static boolean canGenerate( + final DynamoDBMapperTableModel model, + final T object, + final SaveBehavior saveBehavior, + final DynamoDBMapperFieldModel field + ) { + if (field.getGenerateStrategy() == null) { + return false; + } else if (field.getGenerateStrategy() == DynamoDBAutoGenerateStrategy.ALWAYS) { + return true; + } else if (field.get(object) != null) { + return false; + } else if (field.keyType() != null || field.indexed()) { + return true; + } else if (saveBehavior == SaveBehavior.CLOBBER + || saveBehavior == SaveBehavior.UPDATE + || saveBehavior == SaveBehavior.PUT) { + return true; + } else if (anyKeyGeneratable(model, object, saveBehavior)) { + return true; + } + return false; + } + + private final class ValueUpdate { + private final DynamoDBMapperFieldModel field; + private final AttributeValue newValue; + private final Object target; + + public ValueUpdate( + DynamoDBMapperFieldModel field, + AttributeValue newValue, + Object target) { + + this.field = field; + this.newValue = newValue; + this.target = target; + } + + public void apply() { + field.set(target, field.unconvert(newValue)); + } + } + + @Override + public PaginatedScanList scan(Class clazz, + DynamoDBScanExpression scanExpression, + DynamoDBMapperConfig config) { + config = mergeConfig(config); + + ScanRequest scanRequest = createScanRequestFromExpression(clazz, scanExpression, config); + + ScanResponse scanResult = db.scan(scanRequest); + return new PaginatedScanList(this, clazz, db, scanRequest, scanResult, config.getPaginationLoadingStrategy(), config); + } + + @Override + public PaginatedParallelScanList parallelScan(Class clazz, + DynamoDBScanExpression scanExpression, + int totalSegments, + DynamoDBMapperConfig config) { + config = mergeConfig(config); + + // Create hard copies of the original scan request with difference segment number. + List parallelScanRequests = createParallelScanRequestsFromExpression(clazz, scanExpression, totalSegments, config); + ParallelScanTask parallelScanTask = new ParallelScanTask(db, parallelScanRequests); + + return new PaginatedParallelScanList(this, clazz, db, parallelScanTask, config.getPaginationLoadingStrategy(), config); + } + + @Override + public ScanResultPage scanPage(Class clazz, + DynamoDBScanExpression scanExpression, + DynamoDBMapperConfig config) { + config = mergeConfig(config); + + ScanRequest scanRequest = createScanRequestFromExpression(clazz, scanExpression, config); + + ScanResponse scanResult = db.scan(scanRequest); + ScanResultPage result = new ScanResultPage(); + List> parameters = + toParameters(scanResult.items(), clazz, scanRequest.tableName(), config); + + result.setResults(marshallIntoObjects(parameters)); + result.setLastEvaluatedKey(lastEvaluatedKeyOrNull(scanResult.hasLastEvaluatedKey(), scanResult.lastEvaluatedKey())); + result.setCount(scanResult.count()); + result.setScannedCount(scanResult.scannedCount()); + result.setConsumedCapacity(scanResult.consumedCapacity()); + + return result; + } + + @Override + public PaginatedQueryList query(Class clazz, + DynamoDBQueryExpression queryExpression, + DynamoDBMapperConfig config) { + config = mergeConfig(config); + + QueryRequest queryRequest = createQueryRequestFromExpression(clazz, queryExpression, config); + + QueryResponse queryResult = db.query(queryRequest); + return new PaginatedQueryList(this, clazz, db, queryRequest, queryResult, config.getPaginationLoadingStrategy(), config); + } + + @Override + public QueryResultPage queryPage(Class clazz, + DynamoDBQueryExpression queryExpression, + DynamoDBMapperConfig config) { + config = mergeConfig(config); + + QueryRequest queryRequest = createQueryRequestFromExpression(clazz, queryExpression, config); + + QueryResponse queryResult = db.query(queryRequest); + QueryResultPage result = new QueryResultPage(); + + List> parameters = + toParameters(queryResult.items(), clazz, queryRequest.tableName(), config); + + result.setResults(marshallIntoObjects(parameters)); + result.setLastEvaluatedKey(lastEvaluatedKeyOrNull(queryResult.hasLastEvaluatedKey(), queryResult.lastEvaluatedKey())); + result.setCount(queryResult.count()); + result.setScannedCount(queryResult.scannedCount()); + result.setConsumedCapacity(queryResult.consumedCapacity()); + + return result; + } + + @Override + public int count(Class clazz, DynamoDBScanExpression scanExpression, DynamoDBMapperConfig config) { + config = mergeConfig(config); + + ScanRequest scanRequest = createScanRequestFromExpression(clazz, scanExpression, config) + .toBuilder().select(Select.COUNT).build(); + + // Count scans can also be truncated for large datasets + int count = 0; + ScanResponse scanResult = null; + do { + scanResult = db.scan(scanRequest); + count += scanResult.count(); + scanRequest = scanRequest.toBuilder().exclusiveStartKey(scanResult.lastEvaluatedKey()).build(); + } while (scanResult.hasLastEvaluatedKey() && !scanResult.lastEvaluatedKey().isEmpty()); + + return count; + } + + @Override + public int count(Class clazz, DynamoDBQueryExpression queryExpression, DynamoDBMapperConfig config) { + config = mergeConfig(config); + + QueryRequest queryRequest = createQueryRequestFromExpression(clazz, queryExpression, config) + .toBuilder().select(Select.COUNT).build(); + + // Count queries can also be truncated for large datasets + int count = 0; + QueryResponse queryResult = null; + do { + queryResult = db.query(queryRequest); + count += queryResult.count(); + queryRequest = queryRequest.toBuilder().exclusiveStartKey(queryResult.lastEvaluatedKey()).build(); + } while (queryResult.hasLastEvaluatedKey() && !queryResult.lastEvaluatedKey().isEmpty()); + + return count; + } + + /** + * @param config never null + */ + private ScanRequest createScanRequestFromExpression(Class clazz, DynamoDBScanExpression scanExpression, DynamoDBMapperConfig config) { + return ScanRequest.builder() + .tableName(getTableName(clazz, config)) + .indexName(scanExpression.getIndexName()) + .scanFilter(scanExpression.getScanFilter()) + .limit(scanExpression.getLimit()) + .exclusiveStartKey(scanExpression.getExclusiveStartKey()) + .totalSegments(scanExpression.getTotalSegments()) + .segment(scanExpression.getSegment()) + .conditionalOperator(scanExpression.getConditionalOperator()) + .filterExpression(scanExpression.getFilterExpression()) + .expressionAttributeNames(scanExpression.getExpressionAttributeNames()) + .expressionAttributeValues(scanExpression.getExpressionAttributeValues()) + .select(scanExpression.getSelect()) + .projectionExpression(scanExpression.getProjectionExpression()) + .returnConsumedCapacity(scanExpression.getReturnConsumedCapacity()) + .consistentRead(scanExpression.isConsistentRead()) + .build(); + } + + /** + * @param config never null + */ + private List createParallelScanRequestsFromExpression(Class clazz, DynamoDBScanExpression scanExpression, int totalSegments, DynamoDBMapperConfig config) { + if (totalSegments < 1) { + throw new IllegalArgumentException("Parallel scan should have at least one scan segment."); + } + if (scanExpression.getExclusiveStartKey() != null) { + log.info(() -> "The ExclusiveStartKey parameter specified in the DynamoDBScanExpression is ignored," + + " since the individual parallel scan request on each segment is applied on a separate key scope."); + } + if (scanExpression.getSegment() != null || scanExpression.getTotalSegments() != null) { + log.info(() -> "The Segment and TotalSegments parameters specified in the DynamoDBScanExpression are ignored."); + } + + List parallelScanRequests= new LinkedList(); + for (int segment = 0; segment < totalSegments; segment++) { + ScanRequest scanRequest = createScanRequestFromExpression(clazz, scanExpression, config); + parallelScanRequests.add(scanRequest.toBuilder() + .segment(segment).totalSegments(totalSegments) + .exclusiveStartKey(null) + .build()); + } + return parallelScanRequests; + } + + protected QueryRequest createQueryRequestFromExpression(Class clazz, + DynamoDBQueryExpression xpress, DynamoDBMapperConfig config) { + + DynamoDBMapperTableModel model = getTableModel(clazz, config); + + QueryRequest.Builder req = QueryRequest.builder(); + req.consistentRead(xpress.isConsistentRead()); + req.tableName(getTableName(clazz, xpress.getHashKeyValues(), config)); + req.indexName(xpress.getIndexName()); + + req.keyConditionExpression(xpress.getKeyConditionExpression()); + processKeyConditions(req, xpress, model); + + req.scanIndexForward(xpress.isScanIndexForward()) + .limit(xpress.getLimit()) + .exclusiveStartKey(xpress.getExclusiveStartKey()) + .queryFilter(xpress.getQueryFilter()) + .conditionalOperator(xpress.getConditionalOperator()) + .select(xpress.getSelect()) + .projectionExpression(xpress.getProjectionExpression()) + .filterExpression(xpress.getFilterExpression()) + .expressionAttributeNames(xpress.getExpressionAttributeNames()) + .expressionAttributeValues(xpress.getExpressionAttributeValues()) + .returnConsumedCapacity(xpress.getReturnConsumedCapacity()) + ; + return req.build(); + } + + /** + * Utility method for checking the validity of both hash and range key + * conditions. It also tries to infer the correct index name from the POJO + * annotation, if such information is not directly specified by the user. + * + * @param clazz + * The domain class of the queried items. + * @param queryRequest + * The QueryRequest object to be sent to service. + * @param hashKeyConditions + * All the hash key EQ conditions extracted from the POJO object. + * The mapper will choose one of them that could be applied together with + * the user-specified (if any) index name and range key conditions. Or it + * throws error if more than one conditions are applicable for the query. + * @param rangeKeyConditions + * The range conditions specified by the user. We currently only + * allow at most one range key condition. + */ + private static void processKeyConditions( + final QueryRequest.Builder queryRequest, + final DynamoDBQueryExpression expression, + final DynamoDBMapperTableModel model + ) { + // Hash key (primary or index) condition + Map hashKeyConditions = new LinkedHashMap(); + if (expression.getHashKeyValues() != null) { + for (DynamoDBMapperFieldModel field : model.fields()) { + if (field.keyType() == HASH || !field.globalSecondaryIndexNames(HASH).isEmpty()) { + Object value = field.get(expression.getHashKeyValues()); + if (value != null) { + hashKeyConditions.put(field.name(), field.eq(value)); + } + } + } + } + + // Range key (primary or index) conditions + Map rangeKeyConditions = expression.getRangeKeyConditions(); + + // There should be least one hash key condition. + String keyCondExpression = expression.getKeyConditionExpression(); + if (keyCondExpression == null) { + if (isNullOrEmpty(hashKeyConditions)) { + throw new IllegalArgumentException( + "Illegal query expression: No hash key condition is found in the query"); + } + } else { + if (!isNullOrEmpty(hashKeyConditions)) { + throw new IllegalArgumentException( + "Illegal query expression: Either the hash key conditions or the key condition expression must be specified but not both."); + } + if (!isNullOrEmpty(rangeKeyConditions)) { + throw new IllegalArgumentException( + "Illegal query expression: The range key conditions can only be specified when the key condition expression is not specified."); + } + // key condition expression is in use + return; + } + // We don't allow multiple range key conditions. + if (rangeKeyConditions != null && rangeKeyConditions.size() > 1) { + throw new IllegalArgumentException( + "Illegal query expression: Conditions on multiple range keys (" + + rangeKeyConditions.keySet().toString() + + ") are found in the query. DynamoDB service only accepts up to ONE range key condition."); + } + boolean hasRangeKeyCondition = (rangeKeyConditions != null) + && (!rangeKeyConditions.isEmpty()); + String userProvidedIndexName = expression.getIndexName(); + String primaryHashKeyName = model.hashKey().name(); + + // First collect the names of all the global/local secondary indexes that could be applied to this query. + // If the user explicitly specified an index name, we also need to + // 1) check the index is applicable for both hash and range key conditions + // 2) choose one hash key condition if there are more than one of them + boolean hasPrimaryHashKeyCondition = false; + Map> annotatedGSIsOnHashKeys = new HashMap>(); + String hashKeyNameForThisQuery = null; + + boolean hasPrimaryRangeKeyCondition = false; + Set annotatedLSIsOnRangeKey = new HashSet(); + Set annotatedGSIsOnRangeKey = new HashSet(); + + // Range key condition + String rangeKeyNameForThisQuery = null; + if (hasRangeKeyCondition) { + for (String rangeKeyName : rangeKeyConditions.keySet()) { + rangeKeyNameForThisQuery = rangeKeyName; + + DynamoDBMapperFieldModel rk = model.field(rangeKeyName); + + if (rk.keyType() == RANGE) { + hasPrimaryRangeKeyCondition = true; + } + + annotatedLSIsOnRangeKey.addAll(rk.localSecondaryIndexNames()); + annotatedGSIsOnRangeKey.addAll(rk.globalSecondaryIndexNames(RANGE)); + } + + if ( !hasPrimaryRangeKeyCondition + && annotatedLSIsOnRangeKey.isEmpty() + && annotatedGSIsOnRangeKey.isEmpty()) { + throw new DynamoDBMappingException( + "The query contains a condition on a range key (" + + rangeKeyNameForThisQuery + ") " + + "that is not annotated with either @DynamoDBRangeKey or @DynamoDBIndexRangeKey."); + } + } + + boolean userProvidedLSIWithRangeKeyCondition = (userProvidedIndexName != null) + && (annotatedLSIsOnRangeKey.contains(userProvidedIndexName)); + boolean hashOnlyLSIQuery = (userProvidedIndexName != null) + && ( !hasRangeKeyCondition ) + && model.localSecondaryIndex(userProvidedIndexName) != null; + boolean userProvidedLSI = userProvidedLSIWithRangeKeyCondition || hashOnlyLSIQuery; + + boolean userProvidedGSIWithRangeKeyCondition = (userProvidedIndexName != null) + && (annotatedGSIsOnRangeKey.contains(userProvidedIndexName)); + boolean hashOnlyGSIQuery = (userProvidedIndexName != null) + && ( !hasRangeKeyCondition ) + && model.globalSecondaryIndex(userProvidedIndexName) != null; + boolean userProvidedGSI = userProvidedGSIWithRangeKeyCondition || hashOnlyGSIQuery; + + if (userProvidedLSI && userProvidedGSI ) { + throw new DynamoDBMappingException( + "Invalid query: " + + "Index \"" + userProvidedIndexName + "\" " + + "is annotateded as both a LSI and a GSI for attribute."); + } + + // Hash key conditions + for (String hashKeyName : hashKeyConditions.keySet()) { + if (hashKeyName.equals(primaryHashKeyName)) { + hasPrimaryHashKeyCondition = true; + } + + DynamoDBMapperFieldModel hk = model.field(hashKeyName); + + Collection annotatedGSINames = hk.globalSecondaryIndexNames(HASH); + annotatedGSIsOnHashKeys.put(hashKeyName, + annotatedGSINames == null ? new HashSet() : new HashSet(annotatedGSINames)); + + // Additional validation if the user provided an index name. + if (userProvidedIndexName != null) { + boolean foundHashKeyConditionValidWithUserProvidedIndex = false; + if (userProvidedLSI && hashKeyName.equals(primaryHashKeyName)) { + // found an applicable hash key condition (primary hash + LSI range) + foundHashKeyConditionValidWithUserProvidedIndex = true; + } else if (userProvidedGSI && + annotatedGSINames != null && annotatedGSINames.contains(userProvidedIndexName)) { + // found an applicable hash key condition (GSI hash + range) + foundHashKeyConditionValidWithUserProvidedIndex = true; + } + if (foundHashKeyConditionValidWithUserProvidedIndex) { + if ( hashKeyNameForThisQuery != null ) { + throw new IllegalArgumentException( + "Ambiguous query expression: More than one hash key EQ conditions (" + + hashKeyNameForThisQuery + ", " + hashKeyName + + ") are applicable to the specified index (" + + userProvidedIndexName + "). " + + "Please provide only one of them in the query expression."); + } else { + // found an applicable hash key condition + hashKeyNameForThisQuery = hashKeyName; + } + } + } + } + + // Collate all the key conditions + Map keyConditions = new HashMap(); + + // With user-provided index name + if (userProvidedIndexName != null) { + if (hasRangeKeyCondition + && ( !userProvidedLSI ) + && ( !userProvidedGSI )) { + throw new IllegalArgumentException( + "Illegal query expression: No range key condition is applicable to the specified index (" + + userProvidedIndexName + "). "); + } + if (hashKeyNameForThisQuery == null) { + throw new IllegalArgumentException( + "Illegal query expression: No hash key condition is applicable to the specified index (" + + userProvidedIndexName + "). "); + } + + keyConditions.put(hashKeyNameForThisQuery, hashKeyConditions.get(hashKeyNameForThisQuery)); + if (hasRangeKeyCondition) { + keyConditions.putAll(rangeKeyConditions); + } + } + // Infer the index name by finding the index shared by both hash and range key annotations. + else { + if (hasRangeKeyCondition) { + String inferredIndexName = null; + hashKeyNameForThisQuery = null; + if (hasPrimaryHashKeyCondition && hasPrimaryRangeKeyCondition) { + // Found valid query: primary hash + range key conditions + hashKeyNameForThisQuery = primaryHashKeyName; + } else { + // Intersect the set of all the indexes applicable to the range key + // with the set of indexes applicable to each hash key condition. + for (String hashKeyName : annotatedGSIsOnHashKeys.keySet()) { + boolean foundValidQueryExpressionWithInferredIndex = false; + String indexNameInferredByThisHashKey = null; + if (hashKeyName.equals(primaryHashKeyName)) { + if (annotatedLSIsOnRangeKey.size() == 1) { + // Found valid query (Primary hash + LSI range conditions) + foundValidQueryExpressionWithInferredIndex = true; + indexNameInferredByThisHashKey = annotatedLSIsOnRangeKey.iterator().next(); + } + } + + Set annotatedGSIsOnHashKey = annotatedGSIsOnHashKeys.get(hashKeyName); + // We don't need the data in annotatedGSIsOnHashKeys afterwards, + // so it's safe to do the intersection in-place. + annotatedGSIsOnHashKey.retainAll(annotatedGSIsOnRangeKey); + if (annotatedGSIsOnHashKey.size() == 1) { + // Found valid query (Hash + range conditions on a GSI) + if (foundValidQueryExpressionWithInferredIndex) { + hashKeyNameForThisQuery = hashKeyName; + inferredIndexName = indexNameInferredByThisHashKey; + } + + foundValidQueryExpressionWithInferredIndex = true; + indexNameInferredByThisHashKey = annotatedGSIsOnHashKey.iterator().next(); + } + + if (foundValidQueryExpressionWithInferredIndex) { + if (hashKeyNameForThisQuery != null) { + throw new IllegalArgumentException( + "Ambiguous query expression: Found multiple valid queries: " + + "(Hash: \"" + hashKeyNameForThisQuery + "\", Range: \"" + rangeKeyNameForThisQuery + "\", Index: \"" + inferredIndexName + "\") and " + + "(Hash: \"" + hashKeyName + "\", Range: \"" + rangeKeyNameForThisQuery + "\", Index: \"" + indexNameInferredByThisHashKey + "\")."); + } else { + hashKeyNameForThisQuery = hashKeyName; + inferredIndexName = indexNameInferredByThisHashKey; + } + } + } + } + + if (hashKeyNameForThisQuery != null) { + keyConditions.put(hashKeyNameForThisQuery, hashKeyConditions.get(hashKeyNameForThisQuery)); + keyConditions.putAll(rangeKeyConditions); + queryRequest.indexName(inferredIndexName); + } else { + throw new IllegalArgumentException( + "Illegal query expression: Cannot infer the index name from the query expression."); + } + + } else { + // No range key condition is specified. + if (hashKeyConditions.size() > 1) { + if ( hasPrimaryHashKeyCondition ) { + keyConditions.put(primaryHashKeyName, hashKeyConditions.get(primaryHashKeyName)); + } else { + throw new IllegalArgumentException( + "Ambiguous query expression: More than one index hash key EQ conditions (" + + hashKeyConditions.keySet() + + ") are applicable to the query. " + + "Please provide only one of them in the query expression, or specify the appropriate index name."); + } + + } else { + // Only one hash key condition + String hashKeyName = annotatedGSIsOnHashKeys.keySet().iterator().next(); + if ( !hasPrimaryHashKeyCondition ) { + if (annotatedGSIsOnHashKeys.get(hashKeyName).size() == 1) { + // Set the index if the index hash key is only annotated with one GSI. + queryRequest.indexName(annotatedGSIsOnHashKeys.get(hashKeyName).iterator().next()); + } else if (annotatedGSIsOnHashKeys.get(hashKeyName).size() > 1) { + throw new IllegalArgumentException( + "Ambiguous query expression: More than one GSIs (" + + annotatedGSIsOnHashKeys.get(hashKeyName) + + ") are applicable to the query. " + + "Please specify one of them in your query expression."); + } else { + throw new IllegalArgumentException( + "Illegal query expression: No GSI is found in the @DynamoDBIndexHashKey annotation for attribute " + + "\"" + hashKeyName + "\"."); + } + } + keyConditions.putAll(hashKeyConditions); + } + + } + } + + queryRequest.keyConditions(keyConditions); + } + + private AttributeTransformer.Parameters toParameters( + final Map attributeValues, + final Class modelClass, + final String tableName, + final DynamoDBMapperConfig mapperConfig) { + + return toParameters(attributeValues, false, modelClass, tableName, mapperConfig); + } + + private AttributeTransformer.Parameters toParameters( + final Map attributeValues, + final boolean partialUpdate, + final Class modelClass, + final String tableName, + final DynamoDBMapperConfig mapperConfig) { + + return new TransformerParameters( + getTableModel(modelClass, mapperConfig), + attributeValues, + partialUpdate, + modelClass, + mapperConfig, + tableName); + } + + final List> toParameters( + final List> attributeValues, + final Class modelClass, + final String tableName, + final DynamoDBMapperConfig mapperConfig + ) { + if(attributeValues == null) { + return Collections.emptyList(); + } + + List> rval = new ArrayList>(attributeValues.size()); + + for (Map item : attributeValues) { + rval.add(toParameters(item, modelClass, tableName, mapperConfig)); + } + + return rval; + } + + /** + * The one true implementation of AttributeTransformer.Parameters. + */ + private static class TransformerParameters + implements AttributeTransformer.Parameters { + + private final DynamoDBMapperTableModel model; + private final Map attributeValues; + private final boolean partialUpdate; + private final Class modelClass; + private final DynamoDBMapperConfig mapperConfig; + private final String tableName; + + public TransformerParameters( + final DynamoDBMapperTableModel model, + final Map attributeValues, + final boolean partialUpdate, + final Class modelClass, + final DynamoDBMapperConfig mapperConfig, + final String tableName) { + + this.model = model; + this.attributeValues = + Collections.unmodifiableMap(attributeValues); + this.partialUpdate = partialUpdate; + this.modelClass = modelClass; + this.mapperConfig = mapperConfig; + this.tableName = tableName; + } + + @Override + public Map getAttributeValues() { + return attributeValues; + } + + @Override + public boolean isPartialUpdate() { + return partialUpdate; + } + + @Override + public Class getModelClass() { + return modelClass; + } + + @Override + public DynamoDBMapperConfig getMapperConfig() { + return mapperConfig; + } + + @Override + public String getTableName() { + return tableName; + } + + @Override + public String getHashKeyName() { + return model.hashKey().name(); + } + + @Override + public String getRangeKeyName() { + return model.rangeKeyIfExists() == null ? null : model.rangeKey().name(); + } + } + + private Map untransformAttributes( + final AttributeTransformer.Parameters parameters + ) { + if (transformer != null) { + return transformer.untransform(parameters); + } else { + return parameters.getAttributeValues(); + } + } + + private Map transformAttributes( + final AttributeTransformer.Parameters parameters) { + + if (transformer != null) { + return transformer.transform(parameters); + } else { + return parameters.getAttributeValues(); + } + } + + /** + * Returns a new map object that merges the two sets of expected value + * conditions (user-specified or imposed by the internal implementation of + * DynamoDBMapper). Internal assertion on an attribute will be overridden by + * any user-specified condition on the same attribute. + *

+ * Exception is thrown if the two sets of conditions cannot be combined + * together. + */ + private static Map mergeExpectedAttributeValueConditions( + Map internalAssertions, + Map userProvidedConditions, + String userProvidedConditionOperator) { + // If any of the condition map is null, simply return a copy of the other one. + if ((internalAssertions == null || internalAssertions.isEmpty()) + && (userProvidedConditions == null || userProvidedConditions.isEmpty())) { + return null; + } else if (internalAssertions == null) { + return new HashMap(userProvidedConditions); + } else if (userProvidedConditions == null) { + return new HashMap(internalAssertions); + } + + // Start from a copy of the internal conditions + Map mergedExpectedValues = + new HashMap(internalAssertions); + + // Remove internal conditions that are going to be overlaid by user-provided ones. + for (String attrName : userProvidedConditions.keySet()) { + mergedExpectedValues.remove(attrName); + } + + // All the generated internal conditions must be joined by AND. + // Throw an exception if the user specifies an OR operator, and that the + // internal conditions are not totally overlaid by the user-provided + // ones. + if ( ConditionalOperator.OR.toString().equals(userProvidedConditionOperator) + && !mergedExpectedValues.isEmpty() ) { + throw new IllegalArgumentException("Unable to assert the value of the fields " + + mergedExpectedValues.keySet() + ", since the expected value conditions cannot be combined " + + "with user-specified conditions joined by \"OR\". You can use SaveBehavior.CLOBBER to " + + "skip the assertion on these fields."); + } + + mergedExpectedValues.putAll(userProvidedConditions); + + return mergedExpectedValues; + } + + @Override + public S3ClientCache getS3ClientCache() { + return s3Links.getS3ClientCache(); + } + + @Override + public S3Link createS3Link(Region s3region, String bucketName, String key) { + return s3Links.createS3Link(s3region, bucketName, key); + } + + @Override + public S3Link createS3Link(String s3region, String bucketName, String key) { + return s3Links.createS3Link(s3region, bucketName, key); + } + + @Override + public CreateTableRequest generateCreateTableRequest(Class clazz, DynamoDBMapperConfig config) { + config = mergeConfig(config); + DynamoDBMapperTableModel model = getTableModel(clazz, config); + + CreateTableRequest request = new CreateTableRequest(); + request.setTableName(getTableName(clazz, config)); + request.withKeySchema(new KeySchemaElement(model.hashKey().name(), com.amazonaws.services.dynamodbv2.model.KeyType.HASH)); + if (model.rangeKeyIfExists() != null) { + request.withKeySchema(new KeySchemaElement(model.rangeKey().name(), com.amazonaws.services.dynamodbv2.model.KeyType.RANGE)); + } + request.setGlobalSecondaryIndexes(model.globalSecondaryIndexes()); + request.setLocalSecondaryIndexes(model.localSecondaryIndexes()); + for (DynamoDBMapperFieldModel field : model.fields()) { + if (field.keyType() != null || field.indexed()) { + request.withAttributeDefinitions(new AttributeDefinition() + .withAttributeType(ScalarAttributeType.valueOf(field.attributeType().name())) + .withAttributeName(field.name()) + ); + } + } + return request; + } + + @Override + public DeleteTableRequest generateDeleteTableRequest(Class clazz, DynamoDBMapperConfig config) { + config = mergeConfig(config); + DeleteTableRequest deleteTableRequest = new DeleteTableRequest(); + deleteTableRequest.setTableName(getTableName(clazz, config)); + return deleteTableRequest; + } + + /** + * Creates a new table mapper using this mapper to perform operations. + * @param The object type which this mapper operates. + * @param The hash key value type. + * @param The range key value type; use ? if no range key. + * @param clazz The object class. + * @return The table mapper. + */ + public DynamoDBTableMapper newTableMapper(Class clazz) { + DynamoDBMapperConfig config = mergeConfig(null); + return new DynamoDBTableMapper(this.db, this, config, getTableModel(clazz, config)); + } + + /** + * The return type of batchWrite, batchDelete and batchSave. + * + * It contains the information about the unprocessed items and the + * exception causing the failure. + */ + public static class FailedBatch { + private Map> unprocessedItems; + private Exception exception; + + public void setUnprocessedItems(Map> unprocessedItems) { + this.unprocessedItems = unprocessedItems; + } + + public Map> getUnprocessedItems() { + return unprocessedItems; + } + + public void setException(Exception exception) { + this.exception = exception; + } + + public Exception getException() { + return exception; + } + + private final boolean isRequestEntityTooLarge() { + return exception instanceof SdkException && + RetryUtils.isRequestEntityTooLargeException((SdkException)exception); + } + + private final boolean isThrottling() { + return exception instanceof SdkException && + RetryUtils.isThrottlingException((SdkException)exception); + } + + private final int size() { + int size = 0; + for (List values : unprocessedItems.values()) { + size += values.size(); + } + return size; + } + } + + /** + * Used for batch operations where request data is grouped by table name. + */ + static final class StringListMap extends LinkedHashMap> { + private static final long serialVersionUID = -1L; + + public List getPutIfNotExists(final String key) { + List list = get(key); + if (list == null) { + put(key, (list = new LinkedList())); + } + return list; + } + + public boolean add(final String key, final T value) { + return getPutIfNotExists(key).add(value); + } + + public List> subMaps(final int size, boolean perMap) { + LinkedList> maps = new LinkedList>(); + int index = 0, count = 0; + for (Entry> entry : entrySet()) { + for (T value : entry.getValue()) { + if (index == maps.size()) { + maps.add(new StringListMap()); + } + maps.get(index).add(entry.getKey(), value); + index = perMap ? (++count / size) : (++index % size); + } + } + return maps; + } + } + + /** + * Batch pause. + */ + private static void pause(long delay) { + if (delay <= 0) { + return; + } + try { + Thread.sleep(delay); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw SdkClientException.create(e.getMessage(), e); + } + } + + public static final class BatchGetItemException extends SdkClientException { + private final Map unprocessedKeys; + private final Map> responses; + + public BatchGetItemException(String message, Map unprocessedKeys, Map> responses) { + super(SdkClientException.builder().message(message)); + this.unprocessedKeys = unprocessedKeys; + this.responses = responses; + } + + /** + * Returns a map of tables and their respective keys that were not processed during the operation.. + */ + public Map getUnprocessedKeys() { + return unprocessedKeys; + } + + /** + * Returns a map of the loaded objects. Each key in the map is the name of a DynamoDB table. + * Each value in the map is a list of objects that have been loaded from that table. All + * objects for each table can be cast to the associated user defined type that is + * annotated as mapping that table. + */ + public Map> getResponses() { + return responses; + } + } + + private TransactWriteItem generateTransactWriteItem(TransactionWriteOperation transactionWriteOperation, + List inMemoryUpdates, + DynamoDBMapperConfig config) { + Object objectToWrite = transactionWriteOperation.getObject(); + DynamoDBTransactionWriteExpression writeExpression = transactionWriteOperation.getDynamoDBTransactionWriteExpression(); + ReturnValuesOnConditionCheckFailure returnValuesOnConditionCheckFailure = transactionWriteOperation.getReturnValuesOnConditionCheckFailure(); + TransactionWriteOperationType operationType = transactionWriteOperation.getTransactionWriteOperationType(); + Class clazz = (Class)objectToWrite.getClass(); + String tableName = getTableName(clazz, objectToWrite, config); + Map attributeValues = new HashMap(); + DynamoDBMapperTableModel model = getTableModel(clazz, config); + VersionAttributeConditionExpressionGenerator versionAttributeConditionExpressionGenerator = + new VersionAttributeConditionExpressionGenerator(); + for (DynamoDBMapperFieldModel field : model.fields()) { + AttributeValue currentValue = null; + if (field.versioned()) { + if (writeExpression != null) { + throw SdkClientException.create("A transactional write operation may not also specify a condition " + + "expression if a versioned attribute is present on the " + + "model of the item."); + } else { + Object fieldValue = field.get(objectToWrite); + versionAttributeConditionExpressionGenerator + .appendVersionAttributeToConditionExpression(field, + fieldValue); + currentValue = field.convert(field.generate(field.get(objectToWrite))); + inMemoryUpdates.add(new ValueUpdate(field, currentValue, objectToWrite)); + } + } else if (canGenerate(model, objectToWrite, SaveBehavior.CLOBBER, field)) { + currentValue = field.convert(field.generate(field.get(objectToWrite))); + inMemoryUpdates.add(new ValueUpdate(field, currentValue, objectToWrite)); + } else { + currentValue = field.convert(field.get(objectToWrite)); + } + if (currentValue == null && field.keyType() != null) { + throw new DynamoDBMappingException(clazz.getSimpleName() + "[" + field.name() + "]; null or empty value for primary key"); + } else if (currentValue != null) { + attributeValues.put(field.name(), currentValue); + } + } + DynamoDBTransactionWriteExpression versionAttributeConditionExpression = + versionAttributeConditionExpressionGenerator.getVersionAttributeConditionExpression(); + if (versionAttributeConditionExpression.getConditionExpression() != null) { + writeExpression = versionAttributeConditionExpression; + } + AttributeTransformer.Parameters parameters = + toParameters(attributeValues, clazz, tableName, config); + Map attributeValueMap = transformAttributes(parameters); + + TransactWriteItem.Builder transactWriteItem = TransactWriteItem.builder(); + + switch (operationType) { + case Put: + transactWriteItem.put(generatePut(tableName, attributeValueMap, returnValuesOnConditionCheckFailure, writeExpression)); + break; + case Update: + transactWriteItem.update( + generateUpdate(model, tableName, attributeValueMap, returnValuesOnConditionCheckFailure, writeExpression)); + break; + case ConditionCheck: + transactWriteItem.conditionCheck( + generateConditionCheck(model, tableName, objectToWrite, returnValuesOnConditionCheckFailure, writeExpression)); + break; + case Delete: + transactWriteItem.delete( + generateDelete(model, tableName, objectToWrite, returnValuesOnConditionCheckFailure, writeExpression)); + break; + default: + throw new UnsupportedOperationException("Unsupported operationType: " + operationType + " for object: " + model.convertKey(objectToWrite) + " of type: " + clazz); + } + return transactWriteItem.build(); + } + + private Put generatePut(String tableName, + Map attributeValueMap, + ReturnValuesOnConditionCheckFailure returnValuesOnConditionCheckFailure, + DynamoDBTransactionWriteExpression writeExpression) { + Put.Builder put = Put.builder(); + put.item(attributeValueMap); + put.tableName(tableName); + if (returnValuesOnConditionCheckFailure != null) { + put.returnValuesOnConditionCheckFailure(returnValuesOnConditionCheckFailure); + } + if (writeExpression != null) { + if (writeExpression.getConditionExpression() != null) { + put.conditionExpression(writeExpression.getConditionExpression()); + } + if (!isNullOrEmpty(writeExpression.getExpressionAttributeNames())) { + put.expressionAttributeNames(writeExpression.getExpressionAttributeNames()); + } + if (!isNullOrEmpty(writeExpression.getExpressionAttributeValues())) { + put.expressionAttributeValues(writeExpression.getExpressionAttributeValues()); + } + } + return put.build(); + } + + private Update generateUpdate(DynamoDBMapperTableModel model, + String tableName, + Map attributeValueMap, + ReturnValuesOnConditionCheckFailure returnValuesOnConditionCheckFailure, + DynamoDBTransactionWriteExpression writeExpression) { + Update.Builder update = Update.builder(); + Map expressionAttributeNamesMap = new HashMap(); + Map expressionsAttributeValuesMap = new HashMap(); + if (returnValuesOnConditionCheckFailure != null) { + update.returnValuesOnConditionCheckFailure(returnValuesOnConditionCheckFailure); + } + if (writeExpression != null) { + if (writeExpression.getConditionExpression() != null) { + update.conditionExpression(writeExpression.getConditionExpression()); + } + if (!isNullOrEmpty(writeExpression.getExpressionAttributeNames())) { + expressionAttributeNamesMap.putAll(writeExpression.getExpressionAttributeNames()); + } + if (!isNullOrEmpty(writeExpression.getExpressionAttributeValues())) { + expressionsAttributeValuesMap.putAll(writeExpression.getExpressionAttributeValues()); + } + } + Map keyAttributeValueMap = new HashMap(); + // Copy attributeValueMap + Map nonKeyNonNullAttributeValueMap = new HashMap(attributeValueMap); + // These are the non-key attributes that are present in the model and not in the customer object, + // meaning they're to be removed in this update + List nullValuedNonKeyAttributeNames = new ArrayList(); + + // Categorize modeled fields as key, non-key, and removed + for (DynamoDBMapperFieldModel field : model.fields()) { + if (field.keyType() != null) { + keyAttributeValueMap.put(field.name(), nonKeyNonNullAttributeValueMap.remove(field.name())); + } else if (nonKeyNonNullAttributeValueMap.get(field.name()) == null) { + nullValuedNonKeyAttributeNames.add(field.name()); + nonKeyNonNullAttributeValueMap.remove(field.name()); + } + } + + // Categorize null non-modeled fields (using iterator to avoid ConcurrentModificationException) + Iterator iterator = nonKeyNonNullAttributeValueMap.keySet().iterator(); + while (iterator.hasNext()) { + String key = iterator.next(); + if (nonKeyNonNullAttributeValueMap.get(key) == null) { + nullValuedNonKeyAttributeNames.add(key); + iterator.remove(); + } + } + + update.tableName(tableName); + update.updateExpression(new UpdateExpressionGenerator() + .generateUpdateExpressionAndUpdateAttributeMaps(expressionAttributeNamesMap, + expressionsAttributeValuesMap, + nonKeyNonNullAttributeValueMap, + nullValuedNonKeyAttributeNames)); + update.key(keyAttributeValueMap); + if (expressionAttributeNamesMap.size() > 0) { + update.expressionAttributeNames(expressionAttributeNamesMap); + } + if (expressionsAttributeValuesMap.size() > 0) { + update.expressionAttributeValues(expressionsAttributeValuesMap); + } + return update.build(); + } + + private ConditionCheck generateConditionCheck(DynamoDBMapperTableModel model, + String tableName, + Object objectToConditionCheck, + ReturnValuesOnConditionCheckFailure returnValuesOnConditionCheckFailure, + DynamoDBTransactionWriteExpression writeExpression) { + ConditionCheck.Builder conditionCheck = ConditionCheck.builder(); + conditionCheck.key(model.convertKey(objectToConditionCheck)); + conditionCheck.tableName(tableName); + if (returnValuesOnConditionCheckFailure != null) { + conditionCheck.returnValuesOnConditionCheckFailure(returnValuesOnConditionCheckFailure); + } + if (writeExpression != null) { + conditionCheck.conditionExpression(writeExpression.getConditionExpression()); + if (!isNullOrEmpty(writeExpression.getExpressionAttributeNames())) { + conditionCheck.expressionAttributeNames(writeExpression.getExpressionAttributeNames()); + } + if (!isNullOrEmpty(writeExpression.getExpressionAttributeValues())) { + conditionCheck.expressionAttributeValues(writeExpression.getExpressionAttributeValues()); + } + } + return conditionCheck.build(); + } + + private Delete generateDelete(DynamoDBMapperTableModel model, + String tableName, + Object objectToDelete, + ReturnValuesOnConditionCheckFailure returnValuesOnConditionCheckFailure, + DynamoDBTransactionWriteExpression writeExpression) { + + + Delete.Builder delete = Delete.builder(); + delete.key(model.convertKey(objectToDelete)); + delete.tableName(tableName); + if (returnValuesOnConditionCheckFailure != null) { + delete.returnValuesOnConditionCheckFailure(returnValuesOnConditionCheckFailure); + } + if (writeExpression != null) { + if (writeExpression.getConditionExpression() != null) { + delete.conditionExpression(writeExpression.getConditionExpression()); + } + if (!isNullOrEmpty(writeExpression.getExpressionAttributeNames())) { + delete.expressionAttributeNames(writeExpression.getExpressionAttributeNames()); + } + if (!isNullOrEmpty(writeExpression.getExpressionAttributeValues())) { + delete.expressionAttributeValues(writeExpression.getExpressionAttributeValues()); + } + } + return delete.build(); + } + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBMapperConfig.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBMapperConfig.java new file mode 100644 index 000000000000..6cea1411041d --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBMapperConfig.java @@ -0,0 +1,1153 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.services.dynamodb.model.KeysAndAttributes; +import software.amazon.awssdk.services.dynamodb.model.WriteRequest; +import java.util.List; +import java.util.Map; +import java.util.Random; + +/** + * Immutable configuration object for service call behavior. An instance of this + * configuration is supplied to every {@link DynamoDBMapper} at construction; if + * not provided explicitly, {@link DynamoDBMapperConfig#DEFAULT} is used. New + * instances can be given to the mapper object on individual save, load, and + * delete operations to override the defaults. For example: + * + *
+ * DynamoDBMapper mapper = new DynamoDBMapper(dynamoDBClient);
+ * // Force this read to be consistent
+ * DomainClass obj = mapper.load(DomainClass.class, key, ConsistentReads.CONSISTENT.config());
+ * // Force this save operation to use putItem rather than updateItem
+ * mapper.save(obj, SaveBehavior.CLOBBER.config());
+ * // Save the object into a different table
+ * mapper.save(obj, new TableNameOverride("AnotherTable").config());
+ * // Delete the object even if the version field is out of date
+ * mapper.delete(obj, SaveBehavior.CLOBBER.config());
+ * 
+ */ +@SdkPublicApi +public final class DynamoDBMapperConfig { + + /** + * Default configuration; these defaults are also applied by the mapper + * when only partial configurations are specified. + * + * @see SaveBehavior#UPDATE + * @see ConsistentReads#EVENTUAL + * @see PaginationLoadingStrategy#LAZY_LOADING + * @see DefaultTableNameResolver#INSTANCE + * @see DefaultBatchWriteRetryStrategy#INSTANCE + * @see DefaultBatchLoadRetryStrategy#INSTANCE + * @see DynamoDBTypeConverterFactory#standard + * @see ConversionSchemas#DEFAULT + */ + public static final DynamoDBMapperConfig DEFAULT = builder() + .withSaveBehavior(SaveBehavior.UPDATE) + .withConsistentReads(ConsistentReads.EVENTUAL) + .withPaginationLoadingStrategy(PaginationLoadingStrategy.LAZY_LOADING) + .withTableNameResolver(DefaultTableNameResolver.INSTANCE) + .withBatchWriteRetryStrategy(DefaultBatchWriteRetryStrategy.INSTANCE) + .withBatchLoadRetryStrategy(DefaultBatchLoadRetryStrategy.INSTANCE) + .withTypeConverterFactory(DynamoDBTypeConverterFactory.standard()) + .withConversionSchema(ConversionSchemas.DEFAULT) + .build(); + + /** + * Creates a new empty builder. + */ + public static final Builder builder() { + return new Builder(false); + } + + /** + * A fluent builder for DynamoDBMapperConfig objects. + */ + public static class Builder { + + private SaveBehavior saveBehavior; + private ConsistentReads consistentReads; + private TableNameOverride tableNameOverride; + private TableNameResolver tableNameResolver; + private ObjectTableNameResolver objectTableNameResolver; + private PaginationLoadingStrategy paginationLoadingStrategy; + private ConversionSchema conversionSchema; + private BatchWriteRetryStrategy batchWriteRetryStrategy; + private BatchLoadRetryStrategy batchLoadRetryStrategy; + private DynamoDBTypeConverterFactory typeConverterFactory; + + /** + * Creates a new builder initialized with the {@link #DEFAULT} values. + */ + public Builder() { + this(true); + } + + /** + * Creates a new builder, optionally initialized with the defaults. + */ + private Builder(final boolean defaults) { + if (defaults == true) { + saveBehavior = DEFAULT.getSaveBehavior(); + consistentReads = DEFAULT.getConsistentReads(); + paginationLoadingStrategy = DEFAULT.getPaginationLoadingStrategy(); + conversionSchema = DEFAULT.getConversionSchema(); + batchWriteRetryStrategy = DEFAULT.getBatchWriteRetryStrategy(); + batchLoadRetryStrategy = DEFAULT.getBatchLoadRetryStrategy(); + } + } + + /** + * Merges any non-null configuration values for the specified overrides. + */ + private final Builder merge(final DynamoDBMapperConfig o) { + if (o == null) return this; + if (o.saveBehavior != null) saveBehavior = o.saveBehavior; + if (o.consistentReads != null) consistentReads = o.consistentReads; + if (o.tableNameOverride != null) tableNameOverride = o.tableNameOverride; + if (o.tableNameResolver != null) tableNameResolver = o.tableNameResolver; + if (o.objectTableNameResolver != null) objectTableNameResolver = o.objectTableNameResolver; + if (o.paginationLoadingStrategy != null) paginationLoadingStrategy = o.paginationLoadingStrategy; + if (o.conversionSchema != null) conversionSchema = o.conversionSchema; + if (o.batchWriteRetryStrategy != null) batchWriteRetryStrategy = o.batchWriteRetryStrategy; + if (o.batchLoadRetryStrategy != null) batchLoadRetryStrategy = o.batchLoadRetryStrategy; + if (o.typeConverterFactory != null) typeConverterFactory = o.typeConverterFactory; + return this; + } + + /** + * @return the currently-configured save behavior + */ + public SaveBehavior getSaveBehavior() { + return saveBehavior; + } + + /** + * @param value the new save behavior + */ + public void setSaveBehavior(SaveBehavior value) { + saveBehavior = value; + } + + /** + * @param value the new save behavior + * @return this builder + */ + public Builder withSaveBehavior(SaveBehavior value) { + setSaveBehavior(value); + return this; + } + + + /** + * Returns the consistent read behavior. Currently + * this value is applied only in load and batch load operations of the + * DynamoDBMapper. + * @return the currently-configured consistent read behavior. + */ + public ConsistentReads getConsistentReads() { + return consistentReads; + } + + /** + * Sets the consistent read behavior. Currently + * this value is applied only in load and batch load operations of the + * DynamoDBMapper. + * @param value the new consistent read behavior. + */ + public void setConsistentReads(ConsistentReads value) { + consistentReads = value; + } + + /** + * Sets the consistent read behavior. Currently + * this value is applied only in load and batch load operations of the + * DynamoDBMapper. + * @param value the new consistent read behavior + * @return this builder. + * + */ + public Builder withConsistentReads(ConsistentReads value) { + setConsistentReads(value); + return this; + } + + + /** + * @return the current table name override + */ + public TableNameOverride getTableNameOverride() { + return tableNameOverride; + } + + /** + * @param value the new table name override + */ + public void setTableNameOverride(TableNameOverride value) { + tableNameOverride = value; + } + + /** + * @param value the new table name override + * @return this builder + */ + public Builder withTableNameOverride(TableNameOverride value) { + setTableNameOverride(value); + return this; + } + + + /** + * @return the current table name resolver + */ + public TableNameResolver getTableNameResolver() { + return tableNameResolver; + } + + /** + * @param value the new table name resolver + */ + public void setTableNameResolver(TableNameResolver value) { + tableNameResolver = value; + } + + /** + * @param value the new table name resolver + * @return this builder + */ + public Builder withTableNameResolver(TableNameResolver value) { + setTableNameResolver(value); + return this; + } + + + /** + * @return the current object table name resolver + */ + public ObjectTableNameResolver getObjectTableNameResolver() { + return objectTableNameResolver; + } + + /** + * @param value the new object table name resolver + */ + public void setObjectTableNameResolver(ObjectTableNameResolver value) { + objectTableNameResolver = value; + } + + /** + * @param value the new object table name resolver + * @return this builder + */ + public Builder withObjectTableNameResolver(ObjectTableNameResolver value) { + setObjectTableNameResolver(value); + return this; + } + + /** + * @return the currently-configured pagination loading strategy + */ + public PaginationLoadingStrategy getPaginationLoadingStrategy() { + return paginationLoadingStrategy; + } + + /** + * @param value the new pagination loading strategy + */ + public void setPaginationLoadingStrategy( + PaginationLoadingStrategy value) { + + paginationLoadingStrategy = value; + } + + /** + * @param value the new pagination loading strategy + * @return this builder + */ + public Builder withPaginationLoadingStrategy( + PaginationLoadingStrategy value) { + + setPaginationLoadingStrategy(value); + return this; + } + + + /** + * @return the current conversion schema + */ + public ConversionSchema getConversionSchema() { + return conversionSchema; + } + + /** + * @param value the new conversion schema + */ + public void setConversionSchema(ConversionSchema value) { + conversionSchema = value; + } + + /** + * @param value the new conversion schema + * @return this builder + */ + public Builder withConversionSchema(ConversionSchema value) { + setConversionSchema(value); + return this; + } + + /** + * @return the current BatchWriteRetryStrategy + */ + public BatchWriteRetryStrategy getBatchWriteRetryStrategy() { + return batchWriteRetryStrategy; + } + + /** + * @param value the new BatchWriteRetryStrategy + */ + public void setBatchWriteRetryStrategy( + BatchWriteRetryStrategy value) { + this.batchWriteRetryStrategy = value; + } + + /** + * @param value the new BatchWriteRetryStrategy + * @return this builder + */ + public Builder withBatchWriteRetryStrategy( + BatchWriteRetryStrategy value) { + setBatchWriteRetryStrategy(value); + return this; + } + + public BatchLoadRetryStrategy getBatchLoadRetryStrategy() { + return batchLoadRetryStrategy; + } + + /** + * @param value the new BatchLoadRetryStrategy + */ + public void setBatchLoadRetryStrategy( + BatchLoadRetryStrategy value) { + this.batchLoadRetryStrategy = value; + } + + /** + * @param value the new BatchLoadRetryStrategy + * @return this builder + */ + public Builder withBatchLoadRetryStrategy( + BatchLoadRetryStrategy value) { + //set the no retry strategy if the user overrides the default with null + if (value == null) { + value = NoRetryBatchLoadRetryStrategy.INSTANCE; + } + setBatchLoadRetryStrategy(value); + return this; + } + + /** + * @return the current type-converter factory + */ + public final DynamoDBTypeConverterFactory getTypeConverterFactory() { + return typeConverterFactory; + } + + /** + * @param value the new type-converter factory + */ + public final void setTypeConverterFactory(DynamoDBTypeConverterFactory value) { + this.typeConverterFactory = value; + } + + /** + * The type-converter factory for scalar conversions. + *

To override standard type-conversions,

+ *
+         * DynamoDBMapperConfig config = DynamoDBMapperConfig.builder()
+         *     .withTypeConverterFactory(DynamoDBTypeConverterFactory.standard().override()
+         *         .with(String.class, MyObject.class, new StringToMyObjectConverter())
+         *         .build())
+         *     .build();
+         * 
+ *

Then, on the property, specify the attribute binding,

+ *
+         * @DynamoDBTyped(DynamoDBAttributeType.S)
+         * public MyObject getMyObject()
+         * 
+ * @param value the new type-converter factory + * @return this builder + */ + public final Builder withTypeConverterFactory(DynamoDBTypeConverterFactory value) { + setTypeConverterFactory(value); + return this; + } + + /** + * Builds a new {@code DynamoDBMapperConfig} object. + * + * @return the new, immutable config object + */ + public DynamoDBMapperConfig build() { + return new DynamoDBMapperConfig(this); + } + } + + /** + * Enumeration of behaviors for the save operation. + */ + public static enum SaveBehavior { + /** + * UPDATE will not affect unmodeled attributes on a save operation and a + * null value for the modeled attribute will remove it from that item in + * DynamoDB. + *

+ * Because of the limitation of updateItem request, the implementation + * of UPDATE will send a putItem request when a key-only object is being + * saved, and it will send another updateItem request if the given + * key(s) already exists in the table. + *

+ * By default, the mapper uses UPDATE. + */ + UPDATE, + + /** + * UPDATE_SKIP_NULL_ATTRIBUTES is similar to UPDATE, except that it + * ignores any null value attribute(s) and will NOT remove them from + * that item in DynamoDB. It also guarantees to send only one single + * updateItem request, no matter the object is key-only or not. + */ + UPDATE_SKIP_NULL_ATTRIBUTES, + + /** + * CLOBBER will clear and replace all attributes on save, including unmodeled + * ones, and will also disregard versioned field constraints on conditional writes + * as well as overwriting auto-generated values regardless of existing values. + * If versioning is required, use {@link #PUT}. + */ + CLOBBER, + + /** + * PUT will clear and replace all attributes on save, including unmodeled + * ones, but fails if values do not match what is persisted on conditional writes + * and does not overwrite auto-generated values. + */ + PUT, + + /** + * APPEND_SET treats scalar attributes (String, Number, Binary) the same + * as UPDATE_SKIP_NULL_ATTRIBUTES does. However, for set attributes, it + * will append to the existing attribute value, instead of overriding + * it. Caller needs to make sure that the modeled attribute type matches + * the existing set type, otherwise it would result in a service + * exception. + */ + APPEND_SET; + + public final DynamoDBMapperConfig config() { + return builder().withSaveBehavior(this).build(); + } + }; + + /** + * Enumeration of consistent read behavior. + *

+ * CONSISTENT uses consistent reads, EVENTUAL does not. Consistent reads + * have implications for performance and billing; see the service + * documentation for details. + *

+ * By default, the mapper uses eventual consistency. + */ + public static enum ConsistentReads { + CONSISTENT, + EVENTUAL; + + public final DynamoDBMapperConfig config() { + return builder().withConsistentReads(this).build(); + } + }; + + /** + * Enumeration of pagination loading strategy. + */ + public enum PaginationLoadingStrategy { + /** + * Paginated list is lazily loaded when possible, and all loaded results are kept in the memory. Data will + * only be fetched when accessed so this can be more performant than {@link #EAGER_LOADING} when not all + * data is used. Calls to methods such as {@link List#size()} will cause all results to be fetched from + * the service. + *

+ * By default, the mapper uses LAZY_LOADING. + */ + LAZY_LOADING, + + /** + * Only supports using iterator to read from the paginated list. All other list operations will return + * UnsupportedOperationException immediately. During the iteration, the list will clear all the + * previous results before loading the next page, so that the list will keep at most one page of the loaded results in + * memory. This also means the list could only be iterated once. + *

+ * Use this configuration to reduce the memory overhead when handling + * large DynamoDB items. This is the most performant option when you only need to iterate the results + * of a query. + */ + ITERATION_ONLY, + + /** + * Paginated list will eagerly load all the paginated results from DynamoDB as soon as the list is initialized. This may + * make several service calls when the list is created and is not recommended for large data sets. The benefit of using + * eager loading is that service call penalties are paid up front and you get predictable latencies when accessing the + * list afterwards since all contents are in memory. + */ + EAGER_LOADING; + + public final DynamoDBMapperConfig config() { + return builder().withPaginationLoadingStrategy(this).build(); + } + } + + /** + * Allows overriding the table name declared on a domain class by the + * {@link DynamoDBTable} annotation. + */ + public static final class TableNameOverride { + + private final String tableNameOverride; + private final String tableNamePrefix; + + /** + * Returns a new {@link TableNameOverride} object that will prepend the + * given string to every table name. + */ + public static TableNameOverride withTableNamePrefix( + String tableNamePrefix) { + + return new TableNameOverride(null, tableNamePrefix); + } + + /** + * Returns a new {@link TableNameOverride} object that will replace + * every table name in requests with the given string. + */ + public static TableNameOverride withTableNameReplacement( + String tableNameReplacement) { + + return new TableNameOverride(tableNameReplacement, null); + } + + private TableNameOverride(String tableNameOverride, String tableNamePrefix) { + this.tableNameOverride = tableNameOverride; + this.tableNamePrefix = tableNamePrefix; + } + + /** + * @see TableNameOverride#withTableNameReplacement(String) + */ + public TableNameOverride(String tableNameOverride) { + this(tableNameOverride, null); + } + + /** + * Returns the table name to use for all requests. Exclusive with + * {@link TableNameOverride#getTableNamePrefix()} + * + * @see DynamoDBMapperConfig#getTableNameOverride() + */ + public String getTableName() { + return tableNameOverride; + } + + /** + * Returns the table name prefix to prepend the table name for all + * requests. Exclusive with {@link TableNameOverride#getTableName()} + * + * @see DynamoDBMapperConfig#getTableNameOverride() + */ + public String getTableNamePrefix() { + return tableNamePrefix; + } + + public final DynamoDBMapperConfig config() { + return builder().withTableNameOverride(this).build(); + } + } + + /** + * Interface for a strategy used to determine the table name of an object based on its class. + * This resolver is used when an object isn't available such as in + * {@link DynamoDBMapper#query(Class, DynamoDBQueryExpression)} + * + * @see ObjectTableNameResolver + * @author Raniz + */ + public static interface TableNameResolver { + + /** + * Get the table name for a class. This method is used when an object is not available + * such as when creating requests for scan or query operations. + * + * @param clazz The class to get the table name for + * @param config The {@link DynamoDBMapperConfig} + * @return The table name to use for instances of clazz + */ + public String getTableName(Class clazz, DynamoDBMapperConfig config); + } + + /** + * Interface for a strategy used to determine the table name of an object based on its class. + * This resolver is used when an object is available such as in + * {@link DynamoDBMapper#batchSave(Object...)} + * + * If no table name resolver for objects is set, {@link DynamoDBMapper} reverts to using the + * {@link TableNameResolver} on each object's class. + * + * @see TableNameResolver + * @author Raniz + */ + public static interface ObjectTableNameResolver { + + /** + * Get the table name for an object. + * + * @param object The object to get the table name for + * @param config The {@link DynamoDBMapperConfig} + * @return The table name to use for object + */ + public String getTableName(Object object, DynamoDBMapperConfig config); + + } + + /** + * Default implementation of {@link TableNameResolver} that mimics the behavior + * of DynamoDBMapper before the addition of {@link TableNameResolver}. + * + * @author Raniz + */ + public static class DefaultTableNameResolver implements TableNameResolver { + public static final DefaultTableNameResolver INSTANCE = new DefaultTableNameResolver(); + + @Override + public String getTableName(Class clazz, DynamoDBMapperConfig config) { + TableNameOverride override = config.getTableNameOverride(); + + if (override != null) { + String tableName = override.getTableName(); + if (tableName != null) { + return tableName; + } + } + + StandardBeanProperties.Beans beans = StandardBeanProperties.of(clazz); + if (beans.properties().tableName() == null) { + throw new DynamoDBMappingException(clazz + " not annotated with @DynamoDBTable"); + } + + String prefix = override == null ? null : override.getTableNamePrefix(); + return prefix == null ? beans.properties().tableName() : prefix + beans.properties().tableName(); + } + + public final DynamoDBMapperConfig config() { + return builder().withTableNameResolver(this).build(); + } + } + + /** + * DynamoDBMapper#batchWrite takes arbitrary number of save/delete requests + * and breaks them into smaller chunks that can be accepted by the service + * API. Each chunk will be sent to DynamoDB via the BatchWriteItem API, and + * if it fails because the table's provisioned throughput is exceeded or an + * internal processing failure occurs, the failed requests are returned in + * the UnprocessedItems response parameter. This interface allows you to + * control the retry strategy when such scenario occurs. + * + * @see DynamoDBMapper#batchWrite(Iterable, Iterable, DynamoDBMapperConfig) + * @see DynamoDB service API reference -- BatchWriteItem + */ + public interface BatchWriteRetryStrategy { + + /** + * Returns the max number of retries to be performed if the service + * returns UnprocessedItems in the response. + * + * @param batchWriteItemInput + * the one batch of write requests that is being sent to the + * BatchWriteItem API. + * @return max number of retries to be performed if the service returns + * UnprocessedItems in the response, or a negative value if you + * want it to keep retrying until all the UnprocessedItems are + * fulfilled. + */ + public int getMaxRetryOnUnprocessedItems( + Map> batchWriteItemInput); + + /** + * Returns the delay (in milliseconds) before retrying on + * UnprocessedItems. + * + * @param unprocessedItems + * the UnprocessedItems returned by the service in the last + * BatchWriteItem call + * @param retriesAttempted + * The number of times we have attempted to resend + * UnprocessedItems. + * @return the delay (in milliseconds) before resending + * UnprocessedItems. + */ + public long getDelayBeforeRetryUnprocessedItems( + Map> unprocessedItems, + int retriesAttempted); + } + + + /** + * {@link DynamoDBMapper#batchLoad(Iterable, DynamoDBMapperConfig)} breaks the requested items in batches of maximum size 100. + * When calling the Dynamo Db client, there is a chance that due to throttling, some unprocessed keys will be returned. + * This interfaces controls whether we need to retry these unprocessed keys and it also controls the strategy as to how retries should be handled + */ + public interface BatchLoadRetryStrategy { + /** + * Checks if the batch load request should be retried. + * @param batchLoadContext see {@link BatchLoadContext} + * + * @return a boolean true or false value. + */ + public boolean shouldRetry(final BatchLoadContext batchLoadContext); + + /** + * Returns delay(in milliseconds) before retrying Unprocessed keys + * + * @param batchLoadContext see {@link BatchLoadContext} + * @return delay(in milliseconds) before attempting to read unprocessed keys + */ + public long getDelayBeforeNextRetry(final BatchLoadContext batchLoadContext); + } + + /** + * This strategy, like name suggests will not attempt any retries on Unprocessed keys + * + * @author smihir + * + */ + public static class NoRetryBatchLoadRetryStrategy implements BatchLoadRetryStrategy { + public static final NoRetryBatchLoadRetryStrategy INSTANCE = new NoRetryBatchLoadRetryStrategy(); + + /* (non-Javadoc) + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.BatchLoadRetryStrategy#getMaxRetryOnUnprocessedKeys(java.util.Map, java.util.Map) + */ + @Override + public boolean shouldRetry(final BatchLoadContext batchLoadContext) { + return false; + } + + /* (non-Javadoc) + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.BatchLoadRetryStrategy#getDelayBeforeNextRetry(java.util.Map, int) + */ + @Override + public long getDelayBeforeNextRetry(final BatchLoadContext batchLoadContext) { + return -1; + } + + public final DynamoDBMapperConfig config() { + return builder().withBatchLoadRetryStrategy(this).build(); + } + } + + /** + * This is the default strategy. + * If unprocessed keys is equal to requested keys, the request will retried 5 times with a back off strategy + * with maximum back off of 3 seconds + * If few of the keys have been processed, the retries happen without a delay. + * + * @author smihir + * + */ + public static class DefaultBatchLoadRetryStrategy implements BatchLoadRetryStrategy { + public static final DefaultBatchLoadRetryStrategy INSTANCE = new DefaultBatchLoadRetryStrategy(); + + private static final int MAX_RETRIES = 5; + private static final long MAX_BACKOFF_IN_MILLISECONDS = 1000 * 3; + + @Override + public long getDelayBeforeNextRetry(final BatchLoadContext batchLoadContext) { + Map requestedKeys = batchLoadContext.getBatchGetItemRequest().requestItems(); + Map unprocessedKeys = batchLoadContext.getBatchGetItemResult() + .unprocessedKeys(); + + long delay = 0; + //Exponential backoff only when all keys are unprocessed + if (unprocessedKeys != null && requestedKeys != null && unprocessedKeys.size() == requestedKeys.size()) { + Random random = new Random(); + long scaleFactor = 500 + random.nextInt(100); + int retriesAttempted = batchLoadContext.getRetriesAttempted(); + delay = (long) (Math.pow(2, retriesAttempted) * scaleFactor); + delay = Math.min(delay, MAX_BACKOFF_IN_MILLISECONDS); + } + return delay; + } + + @Override + public boolean shouldRetry(BatchLoadContext batchLoadContext) { + Map unprocessedKeys = batchLoadContext.getBatchGetItemResult().unprocessedKeys(); + return (unprocessedKeys != null && unprocessedKeys.size() > 0 && batchLoadContext.getRetriesAttempted() < MAX_RETRIES); + } + + public final DynamoDBMapperConfig config() { + return builder().withBatchLoadRetryStrategy(this).build(); + } + } + + /** + * The default BatchWriteRetryStrategy which always retries on + * UnprocessedItem up to a maximum number of times and use exponential + * backoff with random scale factor. + */ + public static class DefaultBatchWriteRetryStrategy implements BatchWriteRetryStrategy { + public static final DefaultBatchWriteRetryStrategy INSTANCE = new DefaultBatchWriteRetryStrategy(); + + private static final long MAX_BACKOFF_IN_MILLISECONDS = 1000 * 3; + private static final int DEFAULT_MAX_RETRY = -1; + + private final int maxRetry; + + /** + * Keep retrying until success, with default backoff. + */ + public DefaultBatchWriteRetryStrategy() { + this(DEFAULT_MAX_RETRY); + } + + public DefaultBatchWriteRetryStrategy (int maxRetry) { + this.maxRetry = maxRetry; + } + + @Override + public int getMaxRetryOnUnprocessedItems( + Map> batchWriteItemInput) { + return maxRetry; + } + + @Override + public long getDelayBeforeRetryUnprocessedItems( + Map> unprocessedItems, + int retriesAttempted) { + + if (retriesAttempted < 0) { + return 0; + } + + Random random = new Random(); + long scaleFactor = 1000 + random.nextInt(200); + long delay = (long) (Math.pow(2, retriesAttempted) * scaleFactor); + return Math.min(delay, MAX_BACKOFF_IN_MILLISECONDS); + } + + public final DynamoDBMapperConfig config() { + return builder().withBatchWriteRetryStrategy(this).build(); + } + } + + private final SaveBehavior saveBehavior; + private final ConsistentReads consistentReads; + private final TableNameOverride tableNameOverride; + private final TableNameResolver tableNameResolver; + private final ObjectTableNameResolver objectTableNameResolver; + private final PaginationLoadingStrategy paginationLoadingStrategy; + private final ConversionSchema conversionSchema; + private final BatchWriteRetryStrategy batchWriteRetryStrategy; + private final BatchLoadRetryStrategy batchLoadRetryStrategy; + private final DynamoDBTypeConverterFactory typeConverterFactory; + + /** + * Internal constructor; builds from the builder. + */ + private DynamoDBMapperConfig(final DynamoDBMapperConfig.Builder builder) { + this.saveBehavior = builder.saveBehavior; + this.consistentReads = builder.consistentReads; + this.tableNameOverride = builder.tableNameOverride; + this.tableNameResolver = builder.tableNameResolver; + this.objectTableNameResolver = builder.objectTableNameResolver; + this.paginationLoadingStrategy = builder.paginationLoadingStrategy; + this.conversionSchema = builder.conversionSchema; + this.batchWriteRetryStrategy = builder.batchWriteRetryStrategy; + this.batchLoadRetryStrategy = builder.batchLoadRetryStrategy; + this.typeConverterFactory = builder.typeConverterFactory; + } + + /** + * Merges these configuration values with the specified overrides; may + * simply return this instance if overrides are the same or null. + * @param overrides The overrides to merge. + * @return This if the overrides are same or null, or a new merged config. + */ + final DynamoDBMapperConfig merge(final DynamoDBMapperConfig overrides) { + return overrides == null || this == overrides ? this : builder().merge(this).merge(overrides).build(); + } + + /** + * Legacy constructor, using default PaginationLoadingStrategy + * @deprecated in favor of the fluent {@link Builder} + * @see DynamoDBMapperConfig#builder() + **/ + @Deprecated + public DynamoDBMapperConfig( + SaveBehavior saveBehavior, + ConsistentReads consistentReads, + TableNameOverride tableNameOverride) { + + this(saveBehavior, consistentReads, tableNameOverride, (PaginationLoadingStrategy) null); + } + + /** + * Constructs a new configuration object with the save behavior, consistent + * read behavior, and table name override given. + * + * @param saveBehavior + * The {@link SaveBehavior} to use, or null for default. + * @param consistentReads + * The {@link ConsistentReads} to use, or null for default. + * @param tableNameOverride + * An override for the table name, or null for no override. + * @param paginationLoadingStrategy + * The pagination loading strategy, or null for default. + * @deprecated in favor of the fluent {@link Builder} + * @see DynamoDBMapperConfig#builder() + */ + @Deprecated + public DynamoDBMapperConfig( + SaveBehavior saveBehavior, + ConsistentReads consistentReads, + TableNameOverride tableNameOverride, + PaginationLoadingStrategy paginationLoadingStrategy) { + this(saveBehavior, consistentReads, tableNameOverride, null, null, + paginationLoadingStrategy, DEFAULT.getConversionSchema(), + DEFAULT.getBatchWriteRetryStrategy(), DEFAULT.getBatchLoadRetryStrategy()); + } + + private DynamoDBMapperConfig( + SaveBehavior saveBehavior, + ConsistentReads consistentReads, + TableNameOverride tableNameOverride, + TableNameResolver tableNameResolver, + ObjectTableNameResolver objectTableNameResolver, + PaginationLoadingStrategy paginationLoadingStrategy, + ConversionSchema conversionSchema, + BatchWriteRetryStrategy batchWriteRetryStrategy, + BatchLoadRetryStrategy batchLoadRetryStrategy) { + + this.saveBehavior = saveBehavior; + this.consistentReads = consistentReads; + this.tableNameOverride = tableNameOverride; + this.tableNameResolver = tableNameResolver; + this.objectTableNameResolver = objectTableNameResolver; + this.paginationLoadingStrategy = paginationLoadingStrategy; + this.conversionSchema = conversionSchema; + this.batchWriteRetryStrategy = batchWriteRetryStrategy; + this.batchLoadRetryStrategy = batchLoadRetryStrategy; + this.typeConverterFactory = null; + } + + /** + * Constructs a new configuration object with the save behavior given. + * @see SaveBehavior#config + */ + @Deprecated + public DynamoDBMapperConfig(SaveBehavior saveBehavior) { + this(saveBehavior, null, null, null, null, null, + DEFAULT.getConversionSchema(), DEFAULT.getBatchWriteRetryStrategy(), DEFAULT.getBatchLoadRetryStrategy()); + } + + /** + * Constructs a new configuration object with the consistent read behavior + * given. + * @see ConsistentReads#config + */ + @Deprecated + public DynamoDBMapperConfig(ConsistentReads consistentReads) { + this(null, consistentReads, null, null, null, null, + DEFAULT.getConversionSchema(), DEFAULT.getBatchWriteRetryStrategy(), DEFAULT.getBatchLoadRetryStrategy()); + } + + /** + * Constructs a new configuration object with the table name override given. + * @see TableNameOverride#config + */ + @Deprecated + public DynamoDBMapperConfig(TableNameOverride tableNameOverride) { + this(null, null, tableNameOverride, null, null, null, + DEFAULT.getConversionSchema(), DEFAULT.getBatchWriteRetryStrategy(), DEFAULT.getBatchLoadRetryStrategy()); + } + + /** + * Constructs a new configuration object with the table name resolver strategy given. + * @see DynamoDBMapperConfig#builder() + */ + @Deprecated + public DynamoDBMapperConfig(TableNameResolver tableNameResolver) { + this(null, null, null, tableNameResolver, null, null, + DEFAULT.getConversionSchema(), DEFAULT.getBatchWriteRetryStrategy(), DEFAULT.getBatchLoadRetryStrategy()); + } + + /** + * Constructs a new configuration object with the object table name resolver strategy given. + * @see DynamoDBMapperConfig#builder() + */ + @Deprecated + public DynamoDBMapperConfig(ObjectTableNameResolver objectTableNameResolver) { + this(null, null, null, null, objectTableNameResolver, null, + DEFAULT.getConversionSchema(), DEFAULT.getBatchWriteRetryStrategy(), DEFAULT.getBatchLoadRetryStrategy()); + } + + /** + * Constructs a new configuration object with the table name resolver strategies given. + * @see DynamoDBMapperConfig#builder() + */ + @Deprecated + public DynamoDBMapperConfig(TableNameResolver tableNameResolver, ObjectTableNameResolver objectTableNameResolver) { + this(null, null, null, tableNameResolver, objectTableNameResolver, null, + DEFAULT.getConversionSchema(), DEFAULT.getBatchWriteRetryStrategy(), DEFAULT.getBatchLoadRetryStrategy()); + } + + /** + * Constructs a new configuration object with the pagination loading + * strategy given. + * @see PaginationLoadingStrategy#config + */ + @Deprecated + public DynamoDBMapperConfig( + PaginationLoadingStrategy paginationLoadingStrategy) { + + this(null, null, null, null, null, paginationLoadingStrategy, + DEFAULT.getConversionSchema(), DEFAULT.getBatchWriteRetryStrategy(), DEFAULT.getBatchLoadRetryStrategy()); + } + + /** + * Constructs a new configuration object with the conversion schema given. + * @see DynamoDBMapperConfig#builder() + */ + @Deprecated + public DynamoDBMapperConfig(ConversionSchema conversionSchema) { + this(null, null, null, null, null, null, conversionSchema, DEFAULT.getBatchWriteRetryStrategy(), DEFAULT.getBatchLoadRetryStrategy()); + } + + /** + * Constructs a new configuration object from two others: a set of defaults + * and a set of overrides. Any non-null overrides will be applied to the + * defaults. + *

+ * Used internally to merge the {@link DynamoDBMapperConfig} provided at + * construction with an overriding object for a particular operation. + * + * @param defaults + * The default mapper configuration values. + * @param overrides + * The overridden mapper configuration values. Any non-null + * config settings will be applied to the returned object. + * @see DynamoDBMapperConfig#builder() + */ + @Deprecated + public DynamoDBMapperConfig( + DynamoDBMapperConfig defaults, + DynamoDBMapperConfig overrides) { + this(builder().merge(defaults).merge(overrides)); + } + + public BatchLoadRetryStrategy getBatchLoadRetryStrategy() { + return batchLoadRetryStrategy; + } + + /** + * Returns the save behavior for this configuration. + */ + public SaveBehavior getSaveBehavior() { + return saveBehavior; + } + + /** + * Returns the consistent read behavior for this configuration. + */ + public ConsistentReads getConsistentReads() { + return consistentReads; + } + + /** + * Returns the table name override for this configuration. This value will + * override the table name specified in a {@link DynamoDBTable} annotation, + * either by replacing the table name entirely or else by pre-pending a + * string to each table name. This is useful for partitioning data in + * multiple tables at runtime. + * + * @see TableNameOverride#withTableNamePrefix(String) + * @see TableNameOverride#withTableNameReplacement(String) + */ + public TableNameOverride getTableNameOverride() { + return tableNameOverride; + } + + /** + * Returns the table name resolver for this configuration. This value will + * be used to determine the table name for classes. It can be + * used for more powerful customization of table name than is possible using + * only {@link TableNameOverride}. + * + * @see TableNameResolver#getTableName(Class, DynamoDBMapperConfig) + */ + public TableNameResolver getTableNameResolver() { + return tableNameResolver; + } + + /** + * Returns the object table name resolver for this configuration. This value will + * be used to determine the table name for objects. It can be + * used for more powerful customization of table name than is possible using + * only {@link TableNameOverride}. + * + * @see ObjectTableNameResolver#getTableName(Object, DynamoDBMapperConfig) + */ + public ObjectTableNameResolver getObjectTableNameResolver() { + return objectTableNameResolver; + } + + /** + * Returns the pagination loading strategy for this configuration. + */ + public PaginationLoadingStrategy getPaginationLoadingStrategy() { + return paginationLoadingStrategy; + } + + /** + * @return the conversion schema for this config object + */ + public ConversionSchema getConversionSchema() { + return conversionSchema; + } + + /** + * @return the BatchWriteRetryStrategy for this config object + */ + public BatchWriteRetryStrategy getBatchWriteRetryStrategy() { + return batchWriteRetryStrategy; + } + + /** + * @return the current type-converter factory + */ + public final DynamoDBTypeConverterFactory getTypeConverterFactory() { + return typeConverterFactory; + } + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBMapperFieldModel.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBMapperFieldModel.java new file mode 100644 index 000000000000..58d58ade16c7 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBMapperFieldModel.java @@ -0,0 +1,509 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import static software.amazon.awssdk.mapper.dynamodb.DynamoDBAutoGenerateStrategy.ALWAYS; +import static software.amazon.awssdk.mapper.dynamodb.StandardTypeConverters.Vector.LIST; +import static software.amazon.awssdk.services.dynamodb.model.ComparisonOperator.BEGINS_WITH; +import static software.amazon.awssdk.services.dynamodb.model.ComparisonOperator.BETWEEN; +import static software.amazon.awssdk.services.dynamodb.model.ComparisonOperator.CONTAINS; +import static software.amazon.awssdk.services.dynamodb.model.ComparisonOperator.EQ; +import static software.amazon.awssdk.services.dynamodb.model.ComparisonOperator.GE; +import static software.amazon.awssdk.services.dynamodb.model.ComparisonOperator.GT; +import static software.amazon.awssdk.services.dynamodb.model.ComparisonOperator.IN; +import static software.amazon.awssdk.services.dynamodb.model.ComparisonOperator.NULL; +import static software.amazon.awssdk.services.dynamodb.model.ComparisonOperator.LE; +import static software.amazon.awssdk.services.dynamodb.model.ComparisonOperator.LT; +import static software.amazon.awssdk.services.dynamodb.model.ComparisonOperator.NE; +import static software.amazon.awssdk.services.dynamodb.model.ComparisonOperator.NOT_CONTAINS; +import static software.amazon.awssdk.services.dynamodb.model.ComparisonOperator.NOT_NULL; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.ComparisonOperator; +import software.amazon.awssdk.services.dynamodb.model.Condition; +import software.amazon.awssdk.services.dynamodb.model.KeyType; + +import java.util.Arrays; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Field model. + * + * @param The object type. + * @param The field model type. + */ +@SdkPublicApi +public final class DynamoDBMapperFieldModel implements DynamoDBAutoGenerator, DynamoDBTypeConverter { + + public static enum DynamoDBAttributeType { B, N, S, BS, NS, SS, BOOL, NULL, L, M; }; + + private final DynamoDBMapperFieldModel.Properties properties; + private final DynamoDBTypeConverter converter; + private final DynamoDBAttributeType attributeType; + private final DynamoDBMapperFieldModel.Reflect reflect; + + /** + * Creates a new field model instance. + * @param builder The builder. + */ + private DynamoDBMapperFieldModel(final DynamoDBMapperFieldModel.Builder builder) { + this.properties = builder.properties; + this.converter = builder.converter; + this.attributeType = builder.attributeType; + this.reflect = builder.reflect; + } + + /** + * @deprecated replaced by {@link DynamoDBMapperFieldModel#name} + */ + @Deprecated + public String getDynamoDBAttributeName() { + return properties.attributeName(); + } + + /** + * @deprecated replaced by {@link DynamoDBMapperFieldModel#attributeType} + */ + @Deprecated + public DynamoDBAttributeType getDynamoDBAttributeType() { + return attributeType; + } + + /** + * Gets the attribute name. + * @return The attribute name. + */ + public final String name() { + return properties.attributeName(); + } + + /** + * Gets the value from the object instance. + * @param object The object instance. + * @return The value. + */ + public final V get(final T object) { + return reflect.get(object); + } + + /** + * Sets the value on the object instance. + * @param object The object instance. + * @param value The value. + */ + public final void set(final T object, final V value) { + reflect.set(object, value); + } + + /** + * {@inheritDoc} + */ + @Override + public final DynamoDBAutoGenerateStrategy getGenerateStrategy() { + if (properties.autoGenerator() != null) { + return properties.autoGenerator().getGenerateStrategy(); + } + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public final V generate(final V currentValue) { + return properties.autoGenerator().generate(currentValue); + } + + /** + * {@inheritDoc} + */ + @Override + public final AttributeValue convert(final V object) { + return converter.convert(object); + } + + /** + * {@inheritDoc} + */ + @Override + public final V unconvert(final AttributeValue object) { + return converter.unconvert(object); + } + + /** + * Get the current value from the object and convert it. + * @param object The object instance. + * @return The converted value. + */ + public final AttributeValue getAndConvert(final T object) { + return convert(get(object)); + } + + /** + * Unconverts the value and sets it on the object. + * @param object The object instance. + * @param value The attribute value. + */ + public final void unconvertAndSet(final T object, final AttributeValue value) { + set(object, unconvert(value)); + } + + /** + * Gets the DynamoDB attribute type. + * @return The DynamoDB attribute type. + */ + public final DynamoDBAttributeType attributeType() { + return attributeType; + } + + /** + * Gets the key type. + * @return The key type if a key field, null otherwise. + */ + public final KeyType keyType() { + return properties.keyType(); + } + + /** + * Indicates if this attribute is a version attribute. + * @return True if it is, false otherwise. + */ + public final boolean versioned() { + return properties.versioned(); + } + + /** + * Gets the global secondary indexes. + * @param keyType The key type. + * @return The list of global secondary indexes. + */ + public final List globalSecondaryIndexNames(final KeyType keyType) { + if (properties.globalSecondaryIndexNames().containsKey(keyType)) { + return properties.globalSecondaryIndexNames().get(keyType); + } + return Collections.emptyList(); + } + + /** + * Gets the local secondary indexes. + * @return The list of local secondary indexes. + */ + public final List localSecondaryIndexNames() { + return properties.localSecondaryIndexNames(); + } + + /** + * Returns true if the field has any indexes. + * @return True if the propery matches. + */ + public final boolean indexed() { + return !properties.globalSecondaryIndexNames().isEmpty() || !properties.localSecondaryIndexNames().isEmpty(); + } + + /** + * Creates a condition which filters on the specified value. + * @param value The value. + * @return The condition. + * @see com.amazonaws.services.dynamodbv2.model.ComparisonOperator#BEGINS_WITH + * @see com.amazonaws.services.dynamodbv2.model.Condition + */ + public final Condition beginsWith(final V value) { + return Condition.builder().comparisonOperator(BEGINS_WITH).attributeValueList(convert(value)).build(); + } + + /** + * Creates a condition which filters on the specified values. + * @param lo The start of the range (inclusive). + * @param hi The end of the range (inclusive). + * @return The condition. + * @see com.amazonaws.services.dynamodbv2.model.ComparisonOperator#BETWEEN + * @see com.amazonaws.services.dynamodbv2.model.Condition + */ + public final Condition between(final V lo, final V hi) { + return Condition.builder().comparisonOperator(BETWEEN).attributeValueList(convert(lo), convert(hi)).build(); + } + + /** + * Creates a condition which filters on the specified value. + * @param value The value. + * @return The condition. + * @see com.amazonaws.services.dynamodbv2.model.ComparisonOperator#CONTAINS + * @see com.amazonaws.services.dynamodbv2.model.Condition + */ + public final Condition contains(final V value) { + return Condition.builder().comparisonOperator(CONTAINS).attributeValueList(convert(value)).build(); + } + + /** + * Creates a condition which filters on the specified value. + * @param value The value. + * @return The condition. + * @see com.amazonaws.services.dynamodbv2.model.ComparisonOperator#EQ + * @see com.amazonaws.services.dynamodbv2.model.Condition + */ + public final Condition eq(final V value) { + return Condition.builder().comparisonOperator(EQ).attributeValueList(convert(value)).build(); + } + + /** + * Creates a condition which filters on the specified value. + * @param value The value. + * @return The condition. + * @see com.amazonaws.services.dynamodbv2.model.ComparisonOperator#GE + * @see com.amazonaws.services.dynamodbv2.model.Condition + */ + public final Condition ge(final V value) { + return Condition.builder().comparisonOperator(GE).attributeValueList(convert(value)).build(); + } + + /** + * Creates a condition which filters on the specified value. + * @param value The value. + * @return The condition. + * @see com.amazonaws.services.dynamodbv2.model.ComparisonOperator#GT + * @see com.amazonaws.services.dynamodbv2.model.Condition + */ + public final Condition gt(final V value) { + return Condition.builder().comparisonOperator(GT).attributeValueList(convert(value)).build(); + } + + /** + * Creates a condition which filters on the specified values. + * @param values The values. + * @return The condition. + * @see com.amazonaws.services.dynamodbv2.model.ComparisonOperator#IN + * @see com.amazonaws.services.dynamodbv2.model.Condition + */ + public final Condition in(final Collection values) { + return Condition.builder().comparisonOperator(IN).attributeValueList(LIST.convert(values, this)).build(); + } + + /** + * Creates a condition which filters on the specified values. + * @param values The values. + * @return The condition. + * @see com.amazonaws.services.dynamodbv2.model.ComparisonOperator#IN + * @see com.amazonaws.services.dynamodbv2.model.Condition + */ + public final Condition in(final V ... values) { + return in(Arrays.asList(values)); + } + + /** + * Creates a condition which filters on the specified value. + * @return The condition. + * @see com.amazonaws.services.dynamodbv2.model.ComparisonOperator#NULL + * @see com.amazonaws.services.dynamodbv2.model.Condition + */ + public final Condition isNull() { + return Condition.builder().comparisonOperator(NULL).build(); + } + + /** + * Creates a condition which filters on the specified value. + * @param value The value. + * @return The condition. + * @see com.amazonaws.services.dynamodbv2.model.ComparisonOperator#LE + * @see com.amazonaws.services.dynamodbv2.model.Condition + */ + public final Condition le(final V value) { + return Condition.builder().comparisonOperator(LE).attributeValueList(convert(value)).build(); + } + + /** + * Creates a condition which filters on the specified value. + * @param value The value. + * @return The condition. + * @see com.amazonaws.services.dynamodbv2.model.ComparisonOperator#LT + * @see com.amazonaws.services.dynamodbv2.model.Condition + */ + public final Condition lt(final V value) { + return Condition.builder().comparisonOperator(LT).attributeValueList(convert(value)).build(); + } + + /** + * Creates a condition which filters on the specified value. + * @param value The value. + * @return The condition. + * @see com.amazonaws.services.dynamodbv2.model.ComparisonOperator#NE + * @see com.amazonaws.services.dynamodbv2.model.Condition + */ + public final Condition ne(final V value) { + return Condition.builder().comparisonOperator(NE).attributeValueList(convert(value)).build(); + } + + /** + * Creates a condition which filters on the specified value. + * @param value The value. + * @return The condition. + * @see com.amazonaws.services.dynamodbv2.model.ComparisonOperator#NOT_CONTAINS + * @see com.amazonaws.services.dynamodbv2.model.Condition + */ + public final Condition notContains(final V value) { + return Condition.builder().comparisonOperator(NOT_CONTAINS).attributeValueList(convert(value)).build(); + } + + /** + * Creates a condition which filters on the specified value. + * @return The condition. + * @see com.amazonaws.services.dynamodbv2.model.ComparisonOperator#NOT_NULL + * @see com.amazonaws.services.dynamodbv2.model.Condition + */ + public final Condition notNull() { + return Condition.builder().comparisonOperator(NOT_NULL).build(); + } + + /** + * Creates a condition which filters on any non-null argument; if {@code lo} + * is null a {@code LE} condition is applied on {@code hi}, if {@code hi} + * is null a {@code GE} condition is applied on {@code lo}. + * @param lo The start of the range (inclusive). + * @param hi The end of the range (inclusive). + * @return The condition or null if both arguments are null. + * @see com.amazonaws.services.dynamodbv2.model.ComparisonOperator#BETWEEN + * @see com.amazonaws.services.dynamodbv2.model.ComparisonOperator#EQ + * @see com.amazonaws.services.dynamodbv2.model.ComparisonOperator#GE + * @see com.amazonaws.services.dynamodbv2.model.ComparisonOperator#LE + * @see com.amazonaws.services.dynamodbv2.model.Condition + */ + public final Condition betweenAny(final V lo, final V hi) { + return lo == null ? (hi == null ? null : le(hi)) : (hi == null ? ge(lo) : (lo.equals(hi) ? eq(lo) : between(lo,hi))); + } + + /** + * {@link DynamoDBMapperFieldModel} builder. + */ + static class Builder { + private final DynamoDBMapperFieldModel.Properties properties; + private DynamoDBTypeConverter converter; + private DynamoDBMapperFieldModel.Reflect reflect; + private DynamoDBAttributeType attributeType; + private Class targetType; + + public Builder(Class targetType, DynamoDBMapperFieldModel.Properties properties) { + this.properties = properties; + this.targetType = targetType; + } + + public final Builder with(DynamoDBTypeConverter converter) { + this.converter = converter; + return this; + } + + public final Builder with(DynamoDBAttributeType attributeType) { + this.attributeType = attributeType; + return this; + } + + public final Builder with(DynamoDBMapperFieldModel.Reflect reflect) { + this.reflect = reflect; + return this; + } + + public final DynamoDBMapperFieldModel build() { + DynamoDBMapperFieldModel result = new DynamoDBMapperFieldModel(this); + if ((result.keyType() != null || result.indexed()) && !result.attributeType().name().matches("[BNS]")) { + throw new DynamoDBMappingException(String.format( + "%s[%s]; only scalar (B, N, or S) type allowed for key", + targetType.getSimpleName(), result.name() + )); + } else if (result.keyType() != null && result.getGenerateStrategy() == ALWAYS) { + throw new DynamoDBMappingException(String.format( + "%s[%s]; auto-generated key and ALWAYS not allowed", + targetType.getSimpleName(), result.name() + )); + } + return result; + } + } + + /** + * The field model properties. + */ + static interface Properties { + public String attributeName(); + public KeyType keyType(); + public boolean versioned(); + public Map> globalSecondaryIndexNames(); + public List localSecondaryIndexNames(); + public DynamoDBAutoGenerator autoGenerator(); + + static final class Immutable implements Properties { + private final String attributeName; + private final KeyType keyType; + private final boolean versioned; + private final Map> globalSecondaryIndexNames; + private final List localSecondaryIndexNames; + private final DynamoDBAutoGenerator autoGenerator; + + public Immutable(final Properties properties) { + this.attributeName = properties.attributeName(); + this.keyType = properties.keyType(); + this.versioned = properties.versioned(); + this.globalSecondaryIndexNames = properties.globalSecondaryIndexNames(); + this.localSecondaryIndexNames = properties.localSecondaryIndexNames(); + this.autoGenerator = properties.autoGenerator(); + } + + @Override + public final String attributeName() { + return this.attributeName; + } + + @Override + public final KeyType keyType() { + return this.keyType; + } + + @Override + public final boolean versioned() { + return this.versioned; + } + + @Override + public final Map> globalSecondaryIndexNames() { + return this.globalSecondaryIndexNames; + } + + @Override + public final List localSecondaryIndexNames() { + return this.localSecondaryIndexNames; + } + + @Override + public final DynamoDBAutoGenerator autoGenerator() { + return this.autoGenerator; + } + } + } + + /** + * Get/set reflection operations. + * @param The object type. + * @param The value type. + */ + static interface Reflect { + public V get(T object); + public void set(T object, V value); + } + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBMapperTableModel.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBMapperTableModel.java new file mode 100644 index 000000000000..862f7526605d --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBMapperTableModel.java @@ -0,0 +1,478 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import static software.amazon.awssdk.services.dynamodb.model.KeyType.HASH; +import static software.amazon.awssdk.services.dynamodb.model.KeyType.RANGE; +import static com.amazonaws.services.dynamodbv2.model.ProjectionType.KEYS_ONLY; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.KeyType; +import com.amazonaws.services.dynamodbv2.model.GlobalSecondaryIndex; +import com.amazonaws.services.dynamodbv2.model.KeySchemaElement; +import com.amazonaws.services.dynamodbv2.model.LocalSecondaryIndex; +import com.amazonaws.services.dynamodbv2.model.Projection; +import com.amazonaws.services.dynamodbv2.model.ProjectionType; + +import java.util.Arrays; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; + +/** + * Table model. + * + * @param The object type. + */ +@SdkPublicApi +public final class DynamoDBMapperTableModel implements DynamoDBTypeConverter,T> { + + private final Map globalSecondaryIndexes; + private final Map localSecondaryIndexes; + private final Map> versions; + private final Map> fields; + private final Map> keys; + private final DynamoDBMapperTableModel.Properties properties; + private final Class targetType; + + /** + * Constructs a new table model for the specified class. + * @param builder The builder. + */ + private DynamoDBMapperTableModel(final DynamoDBMapperTableModel.Builder builder) { + this.globalSecondaryIndexes = builder.globalSecondaryIndexes(); + this.localSecondaryIndexes = builder.localSecondaryIndexes(); + this.versions = builder.versions(); + this.fields = builder.fields(); + this.keys = builder.keys(); + this.properties = builder.properties; + this.targetType = builder.targetType; + } + + /** + * Gets the object type. + * @return The object type. + */ + public Class targetType() { + return this.targetType; + } + + /** + * Gets all the field models for the given class. + * @return The field models. + */ + public Collection> fields() { + return fields.values(); + } + + /** + * Gets the field model for a given attribute. + * @param The field model's value type. + * @param attributeName The attribute name. + * @return The field model. + */ + @SuppressWarnings("unchecked") + public DynamoDBMapperFieldModel field(final String attributeName) { + DynamoDBMapperFieldModel field = (DynamoDBMapperFieldModel)fields.get(attributeName); + if (field == null) { + throw new DynamoDBMappingException( + targetType.getSimpleName() + "[" + attributeName + "]; no mapping for attribute by name" + ); + } + return field; + } + + /** + * Gets all the key field models for the given class. + * @return The field models. + */ + public Collection> keys() { + return keys.values(); + } + + /** + * Gets the hash key field model for the specified type. + * @param The hash key type. + * @return The hash key field model. + * @throws DynamoDBMappingException If the hash key is not present. + */ + @SuppressWarnings("unchecked") + public DynamoDBMapperFieldModel hashKey() { + DynamoDBMapperFieldModel field = (DynamoDBMapperFieldModel)keys.get(HASH); + if (field == null) { + throw new DynamoDBMappingException( + targetType.getSimpleName() + "; no mapping for HASH key" + ); + } + return field; + } + + /** + * Gets the range key field model for the specified type. + * @param The range key type. + * @return The range key field model. + * @throws DynamoDBMappingException If the range key is not present. + */ + @SuppressWarnings("unchecked") + public DynamoDBMapperFieldModel rangeKey() { + DynamoDBMapperFieldModel field = (DynamoDBMapperFieldModel)keys.get(RANGE); + if (field == null) { + throw new DynamoDBMappingException( + targetType.getSimpleName() + "; no mapping for RANGE key" + ); + } + return field; + } + + /** + * Gets the range key field model for the specified type. + * @param The range key type. + * @return The range key field model, or null if not present. + */ + @SuppressWarnings("unchecked") + public DynamoDBMapperFieldModel rangeKeyIfExists() { + return (DynamoDBMapperFieldModel)keys.get(RANGE); + } + + /** + * Gets all the version fields for the given class. + * @return The field models. + */ + public Collection> versions() { + return versions.values(); + } + + /** + * Indicates if this table has any versioned attributes. + * @return True if any versioned attributes, false otherwise. + */ + public boolean versioned() { + return !versions.isEmpty(); + } + + /** + * Gets the global secondary indexes for the given class. + * @return The map of index name to GlobalSecondaryIndexes. + */ + public Collection globalSecondaryIndexes() { + if (globalSecondaryIndexes.isEmpty()) { + return null; + } + Collection copies = new ArrayList(globalSecondaryIndexes.size()); + for (String indexName : globalSecondaryIndexes.keySet()) { + copies.add(globalSecondaryIndex(indexName)); + } + return copies; + } + + /** + * Gets the global secondary index. + * @param indexName The index name. + * @return The global secondary index or null. + */ + public GlobalSecondaryIndex globalSecondaryIndex(final String indexName) { + if (!globalSecondaryIndexes.containsKey(indexName)) { + return null; + } + GlobalSecondaryIndex gsi = globalSecondaryIndexes.get(indexName); + GlobalSecondaryIndex copy = new GlobalSecondaryIndex().withIndexName(gsi.getIndexName()); + copy.withProjection(new Projection().withProjectionType(gsi.getProjection().getProjectionType())); + for (KeySchemaElement key : gsi.getKeySchema()) { + copy.withKeySchema(new KeySchemaElement(key.getAttributeName(), key.getKeyType())); + } + return copy; + } + + /** + * Gets the local secondary indexes for the given class. + * @param indexNames The index names. + * @return The map of index name to LocalSecondaryIndexes. + */ + public Collection localSecondaryIndexes() { + if (localSecondaryIndexes.isEmpty()) { + return null; + } + Collection copies = new ArrayList(localSecondaryIndexes.size()); + for (String indexName : localSecondaryIndexes.keySet()) { + copies.add(localSecondaryIndex(indexName)); + } + return copies; + } + + /** + * Gets the local secondary index by name. + * @param indexNames The index name. + * @return The local secondary index, or null. + */ + public LocalSecondaryIndex localSecondaryIndex(final String indexName) { + if (!localSecondaryIndexes.containsKey(indexName)) { + return null; + } + LocalSecondaryIndex lsi = localSecondaryIndexes.get(indexName); + LocalSecondaryIndex copy = new LocalSecondaryIndex().withIndexName(lsi.getIndexName()); + copy.withProjection(new Projection().withProjectionType(lsi.getProjection().getProjectionType())); + for (KeySchemaElement key : lsi.getKeySchema()) { + copy.withKeySchema(new KeySchemaElement(key.getAttributeName(), key.getKeyType())); + } + return copy; + } + + /** + * {@inheritDoc} + */ + @Override + public Map convert(final T object) { + Map map = new LinkedHashMap(); + for (DynamoDBMapperFieldModel field : fields()) { + try { + AttributeValue value = field.getAndConvert(object); + if (value != null) { + map.put(field.name(), value); + } + } catch (final RuntimeException e) { + throw new DynamoDBMappingException( + targetType.getSimpleName() + "[" + field.name() + "]; could not convert attribute", e + ); + } + } + return map; + } + + /** + * {@inheritDoc} + */ + @Override + public T unconvert(final Map object) { + T result = StandardBeanProperties.DeclaringReflect.newInstance(targetType); + if (!object.isEmpty()) { + for (DynamoDBMapperFieldModel field : fields()) { + try { + AttributeValue value = object.get(field.name()); + if (value != null) { + field.unconvertAndSet(result, value); + } + } catch (final RuntimeException e) { + throw new DynamoDBMappingException( + targetType.getSimpleName() + "[" + field.name() + "]; could not unconvert attribute", e + ); + } + } + } + return result; + } + + /** + * Creates a new object instance with the keys populated. + * @param The hash key type. + * @param The range key type. + * @param hashKey The hash key. + * @param rangeKey The range key (optional if not present on table). + * @return The new instance. + */ + public T createKey(final H hashKey, final R rangeKey) { + T key = StandardBeanProperties.DeclaringReflect.newInstance(targetType); + if (hashKey != null) { + DynamoDBMapperFieldModel hk = hashKey(); + hk.set(key, hashKey); + } + if (rangeKey != null) { + DynamoDBMapperFieldModel rk = rangeKey(); + rk.set(key, rangeKey); + } + return key; + } + + /** + * Creates a new key map from the specified object. + * @param The hash key type. + * @param The range key type. + * @param object The object instance. + * @return The key map. + */ + public Map convertKey(final T key) { + DynamoDBMapperFieldModel hk = this.hashKey(); + DynamoDBMapperFieldModel rk = this.rangeKeyIfExists(); + return this.convertKey(hk.get(key), (rk == null ? (R)null : rk.get(key))); + } + + /** + * Creates a new key map from the specified hash and range key. + * @param The hash key type. + * @param The range key type. + * @param hashKey The hash key. + * @param rangeKey The range key (optional if not present on table). + * @return The key map. + */ + public Map convertKey(final H hashKey, final R rangeKey) { + Map key = new LinkedHashMap(4); + DynamoDBMapperFieldModel hk = this.hashKey(); + AttributeValue hkValue = hashKey == null ? null : hk.convert(hashKey); + if (hkValue != null) { + key.put(hk.name(), hkValue); + } else { + throw new DynamoDBMappingException( + targetType.getSimpleName() + "[" + hk.name() + "]; no HASH key value present" + ); + } + DynamoDBMapperFieldModel rk = this.rangeKeyIfExists(); + AttributeValue rkValue = rangeKey == null ? null : rk.convert(rangeKey); + if (rkValue != null) { + key.put(rk.name(), rkValue); + } else if (rk != null) { + throw new DynamoDBMappingException( + targetType.getSimpleName() + "[" + rk.name() + "]; no RANGE key value present" + ); + } + return key; + } + + /** + * {@link DynamoDBMapperTableModel} builder. + */ + static class Builder { + private final Map> versions; + private final Map> fields; + private final Map> keys; + private final Properties properties; + private final Class targetType; + + public Builder(Class targetType, Properties properties) { + this.versions = new LinkedHashMap>(4); + this.fields = new LinkedHashMap>(); + this.keys = new EnumMap>(KeyType.class); + this.properties = properties; + this.targetType = targetType; + } + + public Builder with(final DynamoDBMapperFieldModel field) { + fields.put(field.name(), field); + if (field.keyType() != null) { + keys.put(field.keyType(), field); + } + if (field.versioned()) { + versions.put(field.name(), field); + } + return this; + } + + public Map globalSecondaryIndexes() { + Map map = new LinkedHashMap(); + for (DynamoDBMapperFieldModel field : fields.values()) { + for (String indexName : field.globalSecondaryIndexNames(HASH)) { + GlobalSecondaryIndex gsi = new GlobalSecondaryIndex().withIndexName(indexName); + if (map.put(indexName, gsi) != null) { + throw new DynamoDBMappingException( + targetType.getSimpleName() + "[" + field.name() + "]; must not duplicate GSI " + indexName + ); + } + gsi.withProjection(new Projection().withProjectionType(KEYS_ONLY)); + gsi.withKeySchema(new KeySchemaElement(field.name(), com.amazonaws.services.dynamodbv2.model.KeyType.HASH)); + } + } + for (DynamoDBMapperFieldModel field : fields.values()) { + for (String indexName : field.globalSecondaryIndexNames(RANGE)) { + GlobalSecondaryIndex gsi = map.get(indexName); + if (gsi == null) { + throw new DynamoDBMappingException( + targetType.getSimpleName() + "[" + field.name() + "]; no HASH key for GSI " + indexName + ); + } + gsi.withKeySchema(new KeySchemaElement(field.name(), com.amazonaws.services.dynamodbv2.model.KeyType.RANGE)); + } + } + if (map.isEmpty()) { + return Collections.emptyMap(); + } + return Collections.unmodifiableMap(map); + } + + public Map localSecondaryIndexes() { + Map map = new LinkedHashMap(); + for (DynamoDBMapperFieldModel field : fields.values()) { + for (String indexName : field.localSecondaryIndexNames()) { + LocalSecondaryIndex lsi = new LocalSecondaryIndex().withIndexName(indexName); + if (map.put(indexName, lsi) != null) { + throw new DynamoDBMappingException( + targetType.getSimpleName() + "[" + field.name() + "]; must not duplicate LSI " + indexName + ); + } + lsi.withProjection(new Projection().withProjectionType(KEYS_ONLY)); + lsi.withKeySchema(new KeySchemaElement(keys.get(HASH).name(), com.amazonaws.services.dynamodbv2.model.KeyType.HASH)); + lsi.withKeySchema(new KeySchemaElement(field.name(), com.amazonaws.services.dynamodbv2.model.KeyType.RANGE)); + } + } + if (map.isEmpty()) { + return Collections.emptyMap(); + } + return Collections.unmodifiableMap(map); + } + + private Map> versions() { + if (versions.isEmpty()) { + return Collections.>emptyMap(); + } + return Collections.unmodifiableMap(versions); + } + + public Map> fields() { + if (fields.isEmpty()) { + return Collections.>emptyMap(); + } + return Collections.unmodifiableMap(fields); + } + + public Map> keys() { + if (keys.isEmpty()) { + return Collections.>emptyMap(); + } + return Collections.unmodifiableMap(keys); + } + + public DynamoDBMapperTableModel build() { + DynamoDBMapperTableModel result = new DynamoDBMapperTableModel(this); + if (properties.tableName() != null) { + result.hashKey(); //<- make sure the hash key is present + } + return result; + } + } + + /** + * The table model properties. + */ + static interface Properties { + public String tableName(); + + static final class Immutable implements Properties { + private final String tableName; + + public Immutable(final Properties properties) { + this.tableName = properties.tableName(); + } + + @Override + public String tableName() { + return this.tableName; + } + } + } + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBMappingException.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBMappingException.java new file mode 100644 index 000000000000..20854811e106 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBMappingException.java @@ -0,0 +1,45 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.mapper.dynamodb; + +import software.amazon.awssdk.annotations.SdkPublicApi; + +/** + * Generic exception for problems occuring when mapping DynamoDB items to Java + * objects or vice versa. Excludes service exceptions. + */ +@SdkPublicApi +public final class DynamoDBMappingException extends RuntimeException { + + private static final long serialVersionUID = -4883173289978517967L; + + public DynamoDBMappingException() { + super(); + } + + public DynamoDBMappingException(String message, Throwable cause) { + super(message, cause); + } + + public DynamoDBMappingException(String message) { + super(message); + } + + public DynamoDBMappingException(Throwable cause) { + super(cause); + } + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBMappingsRegistry.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBMappingsRegistry.java new file mode 100644 index 000000000000..8830f1cab0df --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBMappingsRegistry.java @@ -0,0 +1,105 @@ +/* + * 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.mapper.dynamodb; + +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.mapper.dynamodb.StandardBeanProperties.Bean; + +/** + * Reflection assistant for {@link DynamoDBMapper} + * + * @deprecated Replaced by {@link StandardBeanProperties}/{@link StandardModelFactories} + */ +@Deprecated +@SdkInternalApi +final class DynamoDBMappingsRegistry { + + /** + * The default instance. + */ + private static final DynamoDBMappingsRegistry INSTANCE = new DynamoDBMappingsRegistry(); + + /** + * Gets the default instance. + * @return The default instance. + */ + static final DynamoDBMappingsRegistry instance() { + return INSTANCE; + } + + /** + * The cache of class to mapping definition. + */ + private final ConcurrentMap, Mappings> mappings = new ConcurrentHashMap, Mappings>(); + + /** + * Gets the mapping definition for a given class. + * @param clazz The class. + * @return The mapping definition. + */ + final Mappings mappingsOf(final Class clazz) { + if (!mappings.containsKey(clazz)) { + mappings.putIfAbsent(clazz, new Mappings(clazz)); + } + return mappings.get(clazz); + } + + /** + * Holds the properties for mapping an object. + */ + static final class Mappings { + private final Map byNames = new HashMap(); + private Mappings(final Class clazz) { + for (Map.Entry> bean : StandardBeanProperties.of((Class)clazz).map().entrySet()) { + Mapping mapping = new Mapping(bean.getValue()); + byNames.put(mapping.getAttributeName(), mapping); + } + } + final Collection getMappings() { + return byNames.values(); + } + } + + /** + * Holds the properties for mapping an object attribute. + */ + static final class Mapping { + private final Bean bean; + private Mapping(final Bean bean) { + this.bean = bean; + } + final Method getter() { + return bean.type().getter(); + } + final boolean isPrimaryKey() { + return bean.properties().keyType() != null; + } + final boolean isVersion() { + return bean.properties().versioned(); + } + final String getAttributeName() { + return bean.properties().attributeName(); + } + } + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBMarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBMarshaller.java new file mode 100644 index 000000000000..c08bc0449b3f --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBMarshaller.java @@ -0,0 +1,62 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.annotations.SdkPublicApi; + +/** + * Marshaller interface for storing complex types in DynamoDB as Strings. + * Implementors provide methods to transform instances of a class to and from + * Strings. + * + * @deprecated Replaced by {@link DynamoDBTypeConverter} + * + *

A {@link DynamoDBTypeConverted} with {@link String} as source would + * perform the same conversion. Please consider, if your marshaller is thread + * safe before replacing. In the new implementation, a single instance of + * {@link DynamoDBTypeConverted} is created per field/attribute. In the old, + * an new instance of the marshaller was created for each call to + * {@code marshall} and {@code unmarshall}. If your marshaller/converter is not + * thread safe, it is recomended to specify a converter which will instantiate + * a new marshaller per call.

+ * + *
+ * public class CustomConverter<T> implements DynamoDBTypeConverter<String,T> {
+ *     @Override
+ *     public final String convert(final T object) {
+ *         return ...
+ *     }
+ *     @Override
+ *     public final T unconvert(final String object) {
+ *         return ...
+ *     }
+ * }
+ * 
+ */ +@SdkPublicApi +@Deprecated +public interface DynamoDBMarshaller { + + /** + * Turns an object of type T into its String representation. + */ + public String marshall(T getterReturnResult); + + /** + * Turns a String representation of an object of type T into an object. + */ + public T unmarshall(Class clazz, String obj); +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBMarshalling.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBMarshalling.java new file mode 100644 index 000000000000..8ffe34f6f284 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBMarshalling.java @@ -0,0 +1,106 @@ +/* + * 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.mapper.dynamodb; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Set; + +/** + * Annotation to mark a property as using a custom marshaller. This is required + * when storing anything other than {@link String}s, {@link Number}s, and + * {@link Set}s of the same to DynamoDB. Any object that can be converted into a + * String representation and vice versa can be saved in this manner. This + * annotation can be applied to either the getter method or the class field for + * the specific property. If the annotation is applied directly to the class + * field, the corresponding getter and setter must be declared in the same + * class. + * + * @see DynamoDBMarshaller + * @see JsonMarshaller + * + * @deprecated Replaced by {@link DynamoDBTypeConverted} + * + *

A {@link DynamoDBTypeConverted} with {@link String} as source would + * perform the same conversion. Please consider, if your marshaller is thread + * safe before replacing. In the new implementation, a single instance of + * {@link DynamoDBTypeConverted} is created per field/attribute. In the old, + * an new instance of the marshaller was created for each call to + * {@code marshall} and {@code unmarshall}. If your marshaller/converter is not + * thread safe, it is recomended to specify a converter which will instantiate + * a new marshaller per call.

+ * + *
+ * public class CustomConverter<T> implements DynamoDBTypeConverter<String,T> {
+ *     @Override
+ *     public final String convert(final T object) {
+ *         return ...
+ *     }
+ *     @Override
+ *     public final T unconvert(final String object) {
+ *         return ...
+ *     }
+ * }
+ * 
+ */ +@Deprecated +@DynamoDB +@DynamoDBTypeConverted(converter=DynamoDBMarshalling.Converter.class) +@DynamoDBTyped(DynamoDBMapperFieldModel.DynamoDBAttributeType.S) +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.METHOD}) +public @interface DynamoDBMarshalling { + + /** + * The class of the Marshaller that converts this property to and from a + * String. + */ + Class> marshallerClass(); + + /** + * Marshalling type converter. + */ + static final class Converter implements DynamoDBTypeConverter { + private final Class> marshallerClass; + private final Class targetType; + + public Converter(final Class targetType, final DynamoDBMarshalling annotation) { + this.marshallerClass = (Class>)annotation.marshallerClass(); + this.targetType = targetType; + } + + @Override + public final String convert(final T object) { + return marshaller().marshall(object); + } + + @Override + public final T unconvert(final String object) { + return marshaller().unmarshall(targetType, object); + } + + private DynamoDBMarshaller marshaller() { + try { + return marshallerClass.newInstance(); + } catch (final Exception e) { + throw new DynamoDBMappingException("Unable to instantiate marshaller " + marshallerClass, e); + } + } + } + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBNamed.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBNamed.java new file mode 100644 index 000000000000..d12ce9157853 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBNamed.java @@ -0,0 +1,47 @@ +/* + * 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.mapper.dynamodb; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation for overriding a property's DynamoDB attribute name. + * + *
+ * @DynamoDBNamed("InternalStatus")
+ * public String getStatus()
+ * 
+ * + *

This annotation has the lowest precedence among other property/field + * annotations where {@code attributeName} may be specified.

+ * + *

May be used as a meta-annotation.

+ */ +@DynamoDB +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +public @interface DynamoDBNamed { + + /** + * Use when the name of the attribute as stored in DynamoDB should differ + * from the name used by the getter / setter. + */ + String value(); + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBNativeBoolean.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBNativeBoolean.java new file mode 100644 index 000000000000..e9d9446fb5e5 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBNativeBoolean.java @@ -0,0 +1,44 @@ +/* + * 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.mapper.dynamodb; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An annotation that marks a {@code boolean} or {@code Boolean} attribute + * of a modeled class which should be serialized as a DynamoDB BOOL. For + * backwards compatibility with old versions of the {@code DynamoDBMapper}, + * by default booleans are serialized using the DynamoDB N type, with a value + * of '1' representing 'true' and a value of '0' representing 'false'. + *

+ * Using this annotation on the field definition or getter method definition + * for the attribute will cause it to be serialized as DynamoDB-native BOOL + * type. Old versions of the {@code DynamoDBMapper} which do not know about the + * BOOL type will be unable to read items containing BOOLs, so don't use me + * unless all readers of your table are using an updated version of the mapper. + * + * @deprecated - Replaced by {@link DynamoDBTyped} + */ +@Deprecated +@DynamoDB +@DynamoDBTyped(DynamoDBMapperFieldModel.DynamoDBAttributeType.BOOL) +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.METHOD}) +public @interface DynamoDBNativeBoolean { +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBQueryExpression.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBQueryExpression.java new file mode 100644 index 000000000000..ccc020060f7a --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBQueryExpression.java @@ -0,0 +1,1323 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import java.util.HashMap; +import java.util.Map; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.Condition; +import software.amazon.awssdk.services.dynamodb.model.ConditionalOperator; +import software.amazon.awssdk.services.dynamodb.model.QueryRequest; +import software.amazon.awssdk.services.dynamodb.model.ReturnConsumedCapacity; +import software.amazon.awssdk.services.dynamodb.model.Select; + +/** + * A query expression. + */ +@SdkPublicApi +public final class DynamoDBQueryExpression { + + private boolean consistentRead = true; + private boolean scanIndexForward = true; + private T hashKeyValues; + private Map rangeKeyConditions; + private Map exclusiveStartKey; + private Integer limit; + private String indexName; + private Map queryFilter; + private String conditionalOperator; + + /** + * Evaluates the query results and returns only the desired values. + *

+ * The condition you specify is applied to the items queried; any items that + * do not match the expression are not returned. + */ + private String filterExpression; + + /** + * The condition that specifies the key value(s) for items to be retrieved + * by the Query action. + */ + private String keyConditionExpression; + + /** + * One or more substitution variables for simplifying complex expressions. + */ + private Map expressionAttributeNames; + + /** + * One or more values that can be substituted in an expression. + */ + private Map expressionAttributeValues; + + /** + * The attributes to be returned in the result. You can retrieve all item + * attributes, specific item attributes, the count of matching items, or + * in the case of an index, some or all of the attributes projected into + * the index.

  • ALL_ATTRIBUTES - Returns all of + * the item attributes from the specified table or index. If you query a + * local secondary index, then for each matching item in the index + * DynamoDB will fetch the entire item from the parent table. If the + * index is configured to project all item attributes, then all of the + * data can be obtained from the local secondary index, and no fetching + * is required.

  • ALL_PROJECTED_ATTRIBUTES - + * Allowed only when querying an index. Retrieves all attributes that + * have been projected into the index. If the index is configured to + * project all attributes, this return value is equivalent to specifying + * ALL_ATTRIBUTES.

  • COUNT - + * Returns the number of matching items, rather than the matching items + * themselves.

  • SPECIFIC_ATTRIBUTES - Returns + * only the attributes listed in AttributesToGet. This return + * value is equivalent to specifying AttributesToGet without + * specifying any value for Select.

    If you query a local + * secondary index and request only attributes that are projected into + * that index, the operation will read only the index and not the table. + * If any of the requested attributes are not projected into the local + * secondary index, DynamoDB will fetch each of these attributes from the + * parent table. This extra fetching incurs additional throughput cost + * and latency.

    If you query a global secondary index, you can only + * request attributes that are projected into the index. Global secondary + * index queries cannot fetch attributes from the parent table.

  • + *

If neither Select nor AttributesToGet are + * specified, DynamoDB defaults to ALL_ATTRIBUTES when + * accessing a table, and ALL_PROJECTED_ATTRIBUTES when + * accessing an index. You cannot use both Select and + * AttributesToGet together in a single request, unless the value + * for Select is SPECIFIC_ATTRIBUTES. (This usage is + * equivalent to specifying AttributesToGet without any value for + * Select.) + *

+ * Constraints:
+ * Allowed Values: ALL_ATTRIBUTES, ALL_PROJECTED_ATTRIBUTES, SPECIFIC_ATTRIBUTES, COUNT + */ + private String select; + + /** + * A string that identifies one or more attributes to retrieve from the + * table. These attributes can include scalars, sets, or elements of a + * JSON document. The attributes in the expression must be separated by + * commas.

If no attribute names are specified, then all attributes + * will be returned. If any of the requested attributes are not found, + * they will not appear in the result.

For more information, go to Accessing + * Item Attributes in the Amazon DynamoDB Developer Guide. + */ + private String projectionExpression; + + /** + * A value that if set to TOTAL, the response includes + * ConsumedCapacity data for tables and indexes. If set to + * INDEXES, the response includes ConsumedCapacity + * for indexes. If set to NONE (the default), + * ConsumedCapacity is not included in the response. + *

+ * Constraints:
+ * Allowed Values: INDEXES, TOTAL, NONE + *

+ * If enabled, the underlying request to DynamoDB will include the + * configured parameter value and the low-level response from DynamoDB will + * include the amount of capacity consumed by the query. Currently, the + * consumed capacity is only exposed through the DynamoDBMapper when you + * call {@code DynamoDBMapper.queryPage}, not {@code DynamoDBMapper.query}. + */ + private String returnConsumedCapacity; + + /** + * Returns whether this query uses consistent reads. + */ + public boolean isConsistentRead() { + return consistentRead; + } + + /** + * Sets whether this query uses consistent reads. + */ + public void setConsistentRead(boolean consistentRead) { + this.consistentRead = consistentRead; + } + + /** + * Sets whether this query uses consistent reads and returns a pointer to + * this object for method-chaining. + */ + public DynamoDBQueryExpression withConsistentRead(boolean consistentRead) { + this.consistentRead = consistentRead; + return this; + } + + /** + * Returns whether this query scans forward. + */ + public boolean isScanIndexForward() { + return scanIndexForward; + } + + /** + * Sets whether this query scans forward. + */ + public void setScanIndexForward(boolean scanIndexForward) { + this.scanIndexForward = scanIndexForward; + } + + /** + * Sets whether this query scans forward and returns a pointer to this + * object for method-chaining. + */ + public DynamoDBQueryExpression withScanIndexForward(boolean scanIndexForward) { + this.scanIndexForward = scanIndexForward; + return this; + } + + /** + * Returns the exclusive start key for this query. + */ + public Map getExclusiveStartKey() { + return exclusiveStartKey; + } + + /** + * Sets the exclusive start key for this query. + */ + public void setExclusiveStartKey(Map exclusiveStartKey) { + this.exclusiveStartKey = exclusiveStartKey; + } + + /** + * Sets the exclusive start key for this query and returns a pointer to this + * object for method-chaining. + */ + public DynamoDBQueryExpression withExclusiveStartKey(Map exclusiveStartKey) { + this.exclusiveStartKey = exclusiveStartKey; + return this; + } + + /** + * Returns the maximum number of items to retrieve in each service request + * to DynamoDB. + *

+ * Note that when calling {@code DynamoDBMapper.query}, multiple requests + * are made to DynamoDB if needed to retrieve the entire result set. Setting + * this will limit the number of items retrieved by each request, NOT + * the total number of results that will be retrieved. Use + * {@code DynamoDBMapper.queryPage} to retrieve a single page of items from + * DynamoDB. + */ + public Integer getLimit() { + return limit; + } + + /** + * Sets the maximum number of items to retrieve in each service request to + * DynamoDB. + *

+ * Note that when calling {@code DynamoDBMapper.query}, multiple requests + * are made to DynamoDB if needed to retrieve the entire result set. Setting + * this will limit the number of items retrieved by each request, NOT + * the total number of results that will be retrieved. Use + * {@code DynamoDBMapper.queryPage} to retrieve a single page of items from + * DynamoDB. + */ + public void setLimit(Integer limit) { + this.limit = limit; + } + + /** + * Sets the maximum number of items to retrieve in each service request to + * DynamoDB and returns a pointer to this object for method-chaining. + *

+ * Note that when calling {@code DynamoDBMapper.query}, multiple requests + * are made to DynamoDB if needed to retrieve the entire result set. Setting + * this will limit the number of items retrieved by each request, NOT + * the total number of results that will be retrieved. Use + * {@code DynamoDBMapper.queryPage} to retrieve a single page of items from + * DynamoDB. + */ + public DynamoDBQueryExpression withLimit(Integer limit) { + this.limit = limit; + return this; + } + + /** + * Gets the hash key value(s) for this query. All hash key attributes for + * the table must be specified with this key object. + */ + public T getHashKeyValues() { + return hashKeyValues; + } + + + /** + * Sets the hash key value(s) for this query. All hash key attributes for + * the table must be specified with this key object. + * + * Note 1: Currently the DynamoDBMapper supports only one value per hash key. + * Note 2: Currently the Amazon DynamoDB supports only one hash key per + * table/index. + */ + public void setHashKeyValues(T hashKeyValues) { + this.hashKeyValues = hashKeyValues; + } + + /** + * Sets the hash key value(s) for this query. All hash key attributes for + * the table must be specified with this key object. + */ + public DynamoDBQueryExpression withHashKeyValues(T hashKObject) { + setHashKeyValues(hashKObject); + return this; + } + + /** + * Gets the range key condition for this query. All range key attributes for + * the table must be specified by attribute name in the map. + */ + public Map getRangeKeyConditions() { + return rangeKeyConditions; + } + + /** + * Sets the range key condition for this query. All range key attributes for + * the table must be specified by attribute name in the map. + * + * @param rangeKeyConditions a map from key name to condition + * NOTE: The current DynamoDB service only allows up to one + * range key condition per query. Providing more than one + * range key condition will result in a SdkClientException. + */ + public void setRangeKeyConditions(Map rangeKeyConditions) { + this.rangeKeyConditions = rangeKeyConditions; + } + + /** + * Sets the range key condition for this query. All range key attributes for + * the table must be specified by attribute name in the map. + * + * @param rangeKeyConditions a map from key name to condition + * NOTE: The current DynamoDB service only allows up to one range + * key condition per query. Providing more than one range key + * condition will result in a SdkClientException. + */ + public DynamoDBQueryExpression withRangeKeyConditions(Map rangeKeyConditions) { + setRangeKeyConditions(rangeKeyConditions); + return this; + } + + /** + * Sets one range key condition for this query, using the attribute name of + * the range key. All range key attributes for the table must be specified + * by using {@link DynamoDBRangeKey} or {@link DynamoDBIndexRangeKey} annotations + * before executing the query. + *

+ *
If the attribute is the primary range key
+ *
users should NOT set any index name for this query.
+ *
If the attribute is an index range key
+ *
+ * {@link DynamoDBMapper} will automatically set the index name if the + * range key is annotated as only used by one local secondary index, + * otherwise users must set the index name manually by either + * {@link DynamoDBQueryExpression#setIndexName(String)} or + * {@link DynamoDBQueryExpression#withIndexName(String)}. + *
+ *
+ * + * @param rangeKeyAttributeName + * This can be either the primary range key of the table or an + * index range key. + * + * @param rangeKeyCondition + * Condition specified on the given range key for this query. + */ + public DynamoDBQueryExpression withRangeKeyCondition(String rangeKeyAttributeName, Condition rangeKeyCondition) { + if ( rangeKeyConditions == null ) + rangeKeyConditions = new HashMap(); + rangeKeyConditions.put(rangeKeyAttributeName, rangeKeyCondition); + return this; + } + + /** + * Returns the name of the index to be used by this query. + */ + public String getIndexName() { + return indexName; + } + + /** + * Sets the name of the index to be used by this query. The hash key + * and/or range key of the index must be specified by adding + * {@link DynamoDBIndexHashKey} or {@code DynamoDBIndexRangeKey} + * annotations to the appropriate getter methods of the mapped + * object. + */ + public void setIndexName(String indexName) { + this.indexName = indexName; + } + + /** + * Sets the name of the index to be used by this query. The hash key + * and/or range key of the index must be specified by adding + * {@link DynamoDBIndexHashKey} or {@code DynamoDBIndexRangeKey} + * annotations to the appropriate getter methods of the mapped + * object. + *

+ * Returns a pointer to this object for method-chaining. + */ + public DynamoDBQueryExpression withIndexName(String indexName) { + setIndexName(indexName); + return this; + } + + /** + * Returns the query filter applied on this query. + */ + public Map getQueryFilter() { + return queryFilter; + } + + /** + * Sets the query filter applied on this query. + */ + public void setQueryFilter(Map queryFilter) { + this.queryFilter = queryFilter; + } + + /** + * Sets the query filter applied on this query. + *

Returns a pointer to this object for method-chaining. + */ + public DynamoDBQueryExpression withQueryFilter(Map queryFilter) { + setQueryFilter(queryFilter); + return this; + } + + /** + * Adds a new condition to the the query filter. + *

Returns a pointer to this object for method-chaining. + * + * @param attributeName + * The name of the attribute on which the specified condition + * operates. + * @param condition + * The filter condition applied on the attribute. + */ + public DynamoDBQueryExpression withQueryFilterEntry(String attributeName, Condition condition) { + if (queryFilter == null) { + queryFilter = new HashMap(); + } + queryFilter.put(attributeName, condition); + return this; + } + + /** + * Returns the logical operator on the query filter conditions. + */ + public String getConditionalOperator() { + return conditionalOperator; + } + + /** + * Sets the logical operator on the query filter conditions. + */ + public void setConditionalOperator(String conditionalOperator) { + this.conditionalOperator = conditionalOperator; + } + + /** + * Sets the logical operator on the query filter conditions. + *

Returns a pointer to this object for method-chaining. + */ + public DynamoDBQueryExpression withConditionalOperator(String conditionalOperator) { + setConditionalOperator(conditionalOperator); + return this; + } + + /** + * Sets the logical operator on the query filter conditions. + */ + public void setConditionalOperator(ConditionalOperator conditionalOperator) { + this.conditionalOperator = conditionalOperator.toString(); + } + + /** + * Sets the logical operator on the query filter conditions. + *

Returns a pointer to this object for method-chaining. + */ + public DynamoDBQueryExpression withConditionalOperator(ConditionalOperator conditionalOperator) { + setConditionalOperator(conditionalOperator); + return this; + } + + /** + * Evaluates the query results and returns only the desired values. + *

+ * The condition you specify is applied to the items queried; any items that + * do not match the expression are not returned. + * + * @return Evaluates the query results and returns only the desired values. + *

+ * The condition you specify is applied to the items queried; any + * items that do not match the expression are not returned. + * @see QueryRequest#getFilterExpression() + */ + public String getFilterExpression() { + return filterExpression; + } + + /** + * Evaluates the query results and returns only the desired values. + *

+ * The condition you specify is applied to the items queried; any items that + * do not match the expression are not returned. + * + * @param filterExpression + * Evaluates the query results and returns only the desired + * values. + *

+ * The condition you specify is applied to the items queried; any + * items that do not match the expression are not returned. + * @see QueryRequest#setFilterExpression(String) + */ + public void setFilterExpression(String filterExpression) { + this.filterExpression = filterExpression; + } + + /** + * Evaluates the query results and returns only the desired values. + *

+ * The condition you specify is applied to the items queried; any items that + * do not match the expression are not returned. + *

+ * Returns a reference to this object so that method calls can be chained + * together. + * + * @param filterExpression + * Evaluates the query results and returns only the desired + * values. + *

+ * The condition you specify is applied to the items queried; any + * items that do not match the expression are not returned. + * + * @return A reference to this updated object so that method calls can be + * chained together. + * @see QueryRequest#withFilterExpression(String) + */ + public DynamoDBQueryExpression withFilterExpression( + String filterExpression) { + this.filterExpression = filterExpression; + return this; + } + + /** + * Returns the condition that specifies the key value(s) for items to be + * retrieved by the Query action. + *

+ * The condition must perform an equality test on a single hash key value. + * The condition can also test for one or more range key values. A + * Query can use KeyConditionExpression to retrieve a single + * item with a given hash and range key value, or several items that have + * the same hash key value but different range key values. + *

+ * The hash key equality test is required, and must be specified in the + * following format: + *

+ * hashAttributeName = :hashval + *

+ * If you also want to provide a range key condition, it must be combined + * using AND with the hash key condition. Following is an example, + * using the = comparison operator for the range key: + *

+ * hashAttributeName = :hashval AND + * rangeAttributeName = :rangeval + *

+ * Valid comparisons for the range key condition are as follows: + *

    + *
  • + *

    + * rangeAttributeName = :rangeval - true if + * the range key is equal to :rangeval.

  • + *
  • + *

    + * rangeAttributeName < :rangeval - true if + * the range key is less than :rangeval.

  • + *
  • + *

    + * rangeAttributeName <= :rangeval - true + * if the range key is less than or equal to :rangeval.

  • + *
  • + *

    + * rangeAttributeName > :rangeval - true if + * the range key is greater than :rangeval.

  • + *
  • + *

    + * rangeAttributeName >=:rangeval - true + * if the range key is greater than or equal to :rangeval.

  • + *
  • + *

    + * rangeAttributeName BETWEEN :rangeval1 + * AND :rangeval2 - true if the range key is greater + * than or equal to :rangeval1, and less than or equal to + * :rangeval2.

  • + *
  • + *

    + * begins_with (rangeAttributeName, + * :rangeval) - true if the range key begins with a + * particular operand. Note that the function name begins_with + * is case-sensitive.

  • + *
+ *

+ * Use ExpressionAttributeValues (via {@link #withExpressionAttributeValues(Map)}) to + * replace tokens such as :hashval and :rangeval + * with actual values at runtime. + *

+ * You can optionally use ExpressionAttributeNames (via + * {@link #withExpressionAttributeNames(Map)}) to replace the names of the hash and range + * attributes with placeholder tokens. This might be necessary if an + * attribute name conflicts with a DynamoDB reserved word. For example, the + * following KeyConditionExpression causes an error because + * Size is a reserved word: + *

    + *
  • Size = :myval
  • + *
+ *

+ * To work around this, define a placeholder (such a #myval) to + * represent the attribute name Size. KeyConditionExpression + * then is as follows: + *

    + *
  • #S = + * :myval
  • + *
+ *

+ * For a list of reserved words, see Reserved Words in the Amazon DynamoDB Developer Guide. + *

+ * For more information on ExpressionAttributeNames and + * ExpressionAttributeValues, see Using Placeholders for Attribute Names and Values in the Amazon + * DynamoDB Developer Guide. + *

+ * KeyConditionExpression replaces the legacy KeyConditions + * parameter. + * + * @return The condition that specifies the key value(s) for items to be + * retrieved by the Query action. + *

+ * The condition must perform an equality test on a single hash key + * value. The condition can also test for one or more range key + * values. A Query can use KeyConditionExpression to + * retrieve a single item with a given hash and range key value, or + * several items that have the same hash key value but different + * range key values. + *

+ * The hash key equality test is required, and must be specified in + * the following format: + *

+ * hashAttributeName = :hashval + *

+ * If you also want to provide a range key condition, it must be + * combined using AND with the hash key condition. Following + * is an example, using the = comparison operator for the + * range key: + *

+ * hashAttributeName = :hashval + * AND rangeAttributeName = + * :rangeval + *

+ * Valid comparisons for the range key condition are as follows: + *

    + *
  • + *

    + * rangeAttributeName = :rangeval - + * true if the range key is equal to :rangeval.

  • + *
  • + *

    + * rangeAttributeName < :rangeval - + * true if the range key is less than :rangeval.

  • + *
  • + *

    + * rangeAttributeName <= :rangeval + * - true if the range key is less than or equal to + * :rangeval.

  • + *
  • + *

    + * rangeAttributeName > :rangeval - + * true if the range key is greater than :rangeval.

  • + *
  • + *

    + * rangeAttributeName >= :rangeval + * - true if the range key is greater than or equal to + * :rangeval.

  • + *
  • + *

    + * rangeAttributeName BETWEEN + * :rangeval1 AND :rangeval2 - true + * if the range key is greater than or equal to + * :rangeval1, and less than or equal to + * :rangeval2.

  • + *
  • + *

    + * begins_with (rangeAttributeName, + * :rangeval) - true if the range key begins + * with a particular operand. Note that the function name + * begins_with is case-sensitive.

  • + *
+ *

+ * Use ExpressionAttributeValues (via + * {@link #withExpressionAttributeValues(Map)}) to replace tokens such as + * :hashval and :rangeval with actual + * values at runtime. + *

+ * You can optionally use ExpressionAttributeNames (via + * {@link #withExpressionAttributeNames(Map)}) to replace the names of the hash and + * range attributes with placeholder tokens. This might be necessary + * if an attribute name conflicts with a DynamoDB reserved word. For + * example, the following KeyConditionExpression causes an + * error because Size is a reserved word: + *

    + *
  • Size = :myval
  • + *
+ *

+ * To work around this, define a placeholder (such a + * #myval) to represent the attribute name Size. + * KeyConditionExpression then is as follows: + *

    + *
  • #S = + * :myval
  • + *
+ *

+ * For a list of reserved words, see Reserved Words in the Amazon DynamoDB Developer + * Guide. + *

+ * For more information on ExpressionAttributeNames and + * ExpressionAttributeValues, see Using Placeholders for Attribute Names and Values in the + * Amazon DynamoDB Developer Guide. + *

+ * KeyConditionExpression replaces the legacy + * KeyConditions parameter. + */ + public String getKeyConditionExpression() { + return keyConditionExpression; + } + + /** + * Sets the condition that specifies the key value(s) for items to be + * retrieved by the Query action. + *

+ * The condition must perform an equality test on a single hash key value. + * The condition can also test for one or more range key values. A + * Query can use KeyConditionExpression to retrieve a single + * item with a given hash and range key value, or several items that have + * the same hash key value but different range key values. + *

+ * The hash key equality test is required, and must be specified in the + * following format: + *

+ * hashAttributeName = :hashval + *

+ * If you also want to provide a range key condition, it must be combined + * using AND with the hash key condition. Following is an example, + * using the = comparison operator for the range key: + *

+ * hashAttributeName = :hashval AND + * rangeAttributeName = :rangeval + *

+ * Valid comparisons for the range key condition are as follows: + *

    + *
  • + *

    + * rangeAttributeName = :rangeval - true if + * the range key is equal to :rangeval.

  • + *
  • + *

    + * rangeAttributeName < :rangeval - true if + * the range key is less than :rangeval.

  • + *
  • + *

    + * rangeAttributeName <= :rangeval - true + * if the range key is less than or equal to :rangeval.

  • + *
  • + *

    + * rangeAttributeName > :rangeval - true if + * the range key is greater than :rangeval.

  • + *
  • + *

    + * rangeAttributeName >= :rangeval - true + * if the range key is greater than or equal to :rangeval.

  • + *
  • + *

    + * rangeAttributeName BETWEEN :rangeval1 + * AND :rangeval2 - true if the range key is greater + * than or equal to :rangeval1, and less than or equal to + * :rangeval2.

  • + *
  • + *

    + * begins_with (rangeAttributeName, + * :rangeval) - true if the range key begins with a + * particular operand. Note that the function name begins_with + * is case-sensitive.

  • + *
+ *

+ * Use ExpressionAttributeValues (via {@link #withExpressionAttributeValues(Map)}) to + * replace tokens such as :hashval and :rangeval + * with actual values at runtime. + *

+ * You can optionally use ExpressionAttributeNames via + * {@link #withExpressionAttributeNames(Map)}) to replace the names of the hash and range + * attributes with placeholder tokens. This might be necessary if an + * attribute name conflicts with a DynamoDB reserved word. For example, the + * following KeyConditionExpression causes an error because + * Size is a reserved word: + *

    + *
  • Size = :myval
  • + *
+ *

+ * To work around this, define a placeholder (such a #myval) to + * represent the attribute name Size. KeyConditionExpression + * then is as follows: + *

    + *
  • #S = + * :myval
  • + *
+ *

+ * For a list of reserved words, see Reserved Words in the Amazon DynamoDB Developer Guide. + *

+ * For more information on ExpressionAttributeNames and + * ExpressionAttributeValues, see Using Placeholders for Attribute Names and Values in the Amazon + * DynamoDB Developer Guide. + *

+ * KeyConditionExpression replaces the legacy KeyConditions + * parameter. + *

+ * When a key expression is specified, the corresponding name-map and + * value-map can optionally be specified via {@link #withExpressionAttributeNames(Map)} and + * {@link #withExpressionAttributeValues(Map)}. + */ + public void setKeyConditionExpression(String keyConditionExpression) { + this.keyConditionExpression = keyConditionExpression; + } + + public DynamoDBQueryExpression withKeyConditionExpression( + String keyConditionExpression) { + this.keyConditionExpression = keyConditionExpression; + return this; + } + /** + * One or more substitution variables for simplifying complex expressions. + * + * @return One or more substitution variables for simplifying complex + * expressions. + * @see QueryRequest#getExpressionAttributeNames() + */ + public java.util.Map getExpressionAttributeNames() { + + return expressionAttributeNames; + } + + /** + * One or more substitution variables for simplifying complex expressions. + * + * @param expressionAttributeNames + * One or more substitution variables for simplifying complex + * expressions. + * @see QueryRequest#setExpressionAttributeNames(Map) + */ + public void setExpressionAttributeNames( + java.util.Map expressionAttributeNames) { + this.expressionAttributeNames = expressionAttributeNames; + } + + /** + * One or more substitution variables for simplifying complex expressions. + * + * @param expressionAttributeNames + * One or more substitution variables for simplifying complex + * expressions. + * + * @return A reference to this updated object so that method calls can be + * chained together. + * @see QueryRequest#withExpressionAttributeNames(Map) + */ + public DynamoDBQueryExpression withExpressionAttributeNames( + java.util.Map expressionAttributeNames) { + setExpressionAttributeNames(expressionAttributeNames); + return this; + } + + /** + * One or more substitution variables for simplifying complex expressions. + * The method adds a new key-value pair into ExpressionAttributeNames + * parameter, and returns a reference to this object so that method calls + * can be chained together. + * + * @param key + * The key of the entry to be added into + * ExpressionAttributeNames. + * @param value + * The corresponding value of the entry to be added into + * ExpressionAttributeNames. + * + * @see QueryRequest#addExpressionAttributeNamesEntry(String, String) + */ + public DynamoDBQueryExpression addExpressionAttributeNamesEntry( + String key, String value) { + if (null == this.expressionAttributeNames) { + this.expressionAttributeNames = new java.util.HashMap(); + } + if (this.expressionAttributeNames.containsKey(key)) + throw new IllegalArgumentException("Duplicated keys (" + + key.toString() + ") are provided."); + this.expressionAttributeNames.put(key, value); + return this; + } + + /** + * Removes all the entries added into ExpressionAttributeNames. + *

+ * Returns a reference to this object so that method calls can be chained + * together. + */ + public DynamoDBQueryExpression clearExpressionAttributeNamesEntries() { + this.expressionAttributeNames = null; + return this; + } + + /** + * One or more values that can be substituted in an expression. + * + * @return One or more values that can be substituted in an expression. + * + * @see QueryRequest#getExpressionAttributeValues() + */ + public java.util.Map getExpressionAttributeValues() { + + return expressionAttributeValues; + } + + /** + * One or more values that can be substituted in an expression. + * + * @param expressionAttributeValues + * One or more values that can be substituted in an expression. + * + * @see QueryRequest#setExpressionAttributeValues(Map) + */ + public void setExpressionAttributeValues( + java.util.Map expressionAttributeValues) { + this.expressionAttributeValues = expressionAttributeValues; + } + + /** + * One or more values that can be substituted in an expression. + * + * @param expressionAttributeValues + * One or more values that can be substituted in an expression. + * + * @return A reference to this updated object so that method calls can be + * chained together. + * @see QueryRequest#withExpressionAttributeValues(Map) + */ + public DynamoDBQueryExpression withExpressionAttributeValues( + java.util.Map expressionAttributeValues) { + setExpressionAttributeValues(expressionAttributeValues); + return this; + } + + /** + * One or more values that can be substituted in an expression. The method + * adds a new key-value pair into ExpressionAttributeValues parameter, and + * returns a reference to this object so that method calls can be chained + * together. + * + * @param key + * The key of the entry to be added into + * ExpressionAttributeValues. + * @param value + * The corresponding value of the entry to be added into + * ExpressionAttributeValues. + */ + public DynamoDBQueryExpression addExpressionAttributeValuesEntry( + String key, AttributeValue value) { + if (null == this.expressionAttributeValues) { + this.expressionAttributeValues = new java.util.HashMap(); + } + if (this.expressionAttributeValues.containsKey(key)) + throw new IllegalArgumentException("Duplicated keys (" + + key.toString() + ") are provided."); + this.expressionAttributeValues.put(key, value); + return this; + } + + /** + * Removes all the entries added into ExpressionAttributeValues. + *

+ * Returns a reference to this object so that method calls can be chained + * together. + */ + public DynamoDBQueryExpression clearExpressionAttributeValuesEntries() { + this.expressionAttributeValues = null; + return this; + } + + /** + * The attributes to be returned in the result. You can retrieve all item + * attributes, specific item attributes, the count of matching items, or + * in the case of an index, some or all of the attributes projected into + * the index. + *

+ * Constraints:
+ * Allowed Values: ALL_ATTRIBUTES, ALL_PROJECTED_ATTRIBUTES, SPECIFIC_ATTRIBUTES, COUNT + * + * @return The attributes to be returned in the result. You can retrieve all item + * attributes, specific item attributes, the count of matching items, or + * in the case of an index, some or all of the attributes projected into + * the index. + * + * @see com.amazonaws.services.dynamodbv2.model.Select + */ + public String getSelect() { + return select; + } + + /** + * The attributes to be returned in the result. You can retrieve all item + * attributes, specific item attributes, the count of matching items, or + * in the case of an index, some or all of the attributes projected into + * the index. + *

+ * Constraints:
+ * Allowed Values: ALL_ATTRIBUTES, ALL_PROJECTED_ATTRIBUTES, SPECIFIC_ATTRIBUTES, COUNT + * + * @param select The attributes to be returned in the result. You can retrieve all item + * attributes, specific item attributes, the count of matching items, or + * in the case of an index, some or all of the attributes projected into + * the index. + * + * @see com.amazonaws.services.dynamodbv2.model.Select + */ + public void setSelect(String select) { + this.select = select; + } + + /** + * The attributes to be returned in the result. You can retrieve all item + * attributes, specific item attributes, the count of matching items, or + * in the case of an index, some or all of the attributes projected into + * the index. + *

+ * Returns a reference to this object so that method calls can be chained together. + *

+ * Constraints:
+ * Allowed Values: ALL_ATTRIBUTES, ALL_PROJECTED_ATTRIBUTES, SPECIFIC_ATTRIBUTES, COUNT + * + * @param select The attributes to be returned in the result. You can retrieve all item + * attributes, specific item attributes, the count of matching items, or + * in the case of an index, some or all of the attributes projected into + * the index. + * + * @return A reference to this updated object so that method calls can be chained + * together. + * + * @see com.amazonaws.services.dynamodbv2.model.Select + */ + public DynamoDBQueryExpression withSelect(String select) { + this.select = select; + return this; + } + + /** + * The attributes to be returned in the result. You can retrieve all item + * attributes, specific item attributes, the count of matching items, or + * in the case of an index, some or all of the attributes projected into + * the index. + *

+ * Constraints:
+ * Allowed Values: ALL_ATTRIBUTES, ALL_PROJECTED_ATTRIBUTES, SPECIFIC_ATTRIBUTES, COUNT + * + * @param select The attributes to be returned in the result. You can retrieve all item + * attributes, specific item attributes, the count of matching items, or + * in the case of an index, some or all of the attributes projected into + * the index. + * + * @see com.amazonaws.services.dynamodbv2.model.Select + */ + public void setSelect(Select select) { + this.select = select.toString(); + } + + /** + * The attributes to be returned in the result. You can retrieve all item + * attributes, specific item attributes, the count of matching items, or + * in the case of an index, some or all of the attributes projected into + * the index. + *

+ * Returns a reference to this object so that method calls can be chained together. + *

+ * Constraints:
+ * Allowed Values: ALL_ATTRIBUTES, ALL_PROJECTED_ATTRIBUTES, SPECIFIC_ATTRIBUTES, COUNT + * + * @param select The attributes to be returned in the result. You can retrieve all item + * attributes, specific item attributes, the count of matching items, or + * in the case of an index, some or all of the attributes projected into + * the index. + * + * @return A reference to this updated object so that method calls can be chained + * together. + * + * @see com.amazonaws.services.dynamodbv2.model.Select + */ + public DynamoDBQueryExpression withSelect(Select select) { + this.select = select.toString(); + return this; + } + + /** + * A string that identifies one or more attributes to retrieve from the + * table. These attributes can include scalars, sets, or elements of a + * JSON document. The attributes in the expression must be separated by + * commas.

If no attribute names are specified, then all attributes + * will be returned. If any of the requested attributes are not found, + * they will not appear in the result.

For more information, go to Accessing + * Item Attributes in the Amazon DynamoDB Developer Guide. + * + * @return A string that identifies one or more attributes to retrieve from the + * table. These attributes can include scalars, sets, or elements of a + * JSON document. The attributes in the expression must be separated by + * commas.

If no attribute names are specified, then all attributes + * will be returned. If any of the requested attributes are not found, + * they will not appear in the result.

For more information, go to Accessing + * Item Attributes in the Amazon DynamoDB Developer Guide. + */ + public String getProjectionExpression() { + return projectionExpression; + } + + /** + * A string that identifies one or more attributes to retrieve from the + * table. These attributes can include scalars, sets, or elements of a + * JSON document. The attributes in the expression must be separated by + * commas.

If no attribute names are specified, then all attributes + * will be returned. If any of the requested attributes are not found, + * they will not appear in the result.

For more information, go to Accessing + * Item Attributes in the Amazon DynamoDB Developer Guide. + * + * @param projectionExpression A string that identifies one or more attributes to retrieve from the + * table. These attributes can include scalars, sets, or elements of a + * JSON document. The attributes in the expression must be separated by + * commas.

If no attribute names are specified, then all attributes + * will be returned. If any of the requested attributes are not found, + * they will not appear in the result.

For more information, go to Accessing + * Item Attributes in the Amazon DynamoDB Developer Guide. + */ + public void setProjectionExpression(String projectionExpression) { + this.projectionExpression = projectionExpression; + } + + /** + * A string that identifies one or more attributes to retrieve from the + * table. These attributes can include scalars, sets, or elements of a + * JSON document. The attributes in the expression must be separated by + * commas.

If no attribute names are specified, then all attributes + * will be returned. If any of the requested attributes are not found, + * they will not appear in the result.

For more information, go to Accessing + * Item Attributes in the Amazon DynamoDB Developer Guide. + *

+ * Returns a reference to this object so that method calls can be chained together. + * + * @param projectionExpression A string that identifies one or more attributes to retrieve from the + * table. These attributes can include scalars, sets, or elements of a + * JSON document. The attributes in the expression must be separated by + * commas.

If no attribute names are specified, then all attributes + * will be returned. If any of the requested attributes are not found, + * they will not appear in the result.

For more information, go to Accessing + * Item Attributes in the Amazon DynamoDB Developer Guide. + * + * @return A reference to this updated object so that method calls can be chained + * together. + */ + public DynamoDBQueryExpression withProjectionExpression(String projectionExpression) { + this.projectionExpression = projectionExpression; + return this; + } + + /** + * A value that if set to TOTAL, the response includes + * ConsumedCapacity data for tables and indexes. If set to + * INDEXES, the response includes ConsumedCapacity + * for indexes. If set to NONE (the default), + * ConsumedCapacity is not included in the response. + *

+ * Constraints:
+ * Allowed Values: INDEXES, TOTAL, NONE + *

+ * If enabled, the underlying request to DynamoDB will include the + * configured parameter value and the low-level response from DynamoDB will + * include the amount of capacity consumed by the query. Currently, the + * consumed capacity is only exposed through the DynamoDBMapper when you + * call {@code DynamoDBMapper.queryPage}, not {@code DynamoDBMapper.query}. + * + * @return A value that if set to TOTAL, the response includes + * ConsumedCapacity data for tables and indexes. If set to + * INDEXES, the response includes ConsumedCapacity + * for indexes. If set to NONE (the default), + * ConsumedCapacity is not included in the response. + * + * @see com.amazonaws.services.dynamodbv2.model.ReturnConsumedCapacity + */ + public String getReturnConsumedCapacity() { + return returnConsumedCapacity; + } + + /** + * A value that if set to TOTAL, the response includes + * ConsumedCapacity data for tables and indexes. If set to + * INDEXES, the response includes ConsumedCapacity + * for indexes. If set to NONE (the default), + * ConsumedCapacity is not included in the response. + *

+ * Constraints:
+ * Allowed Values: INDEXES, TOTAL, NONE + *

+ * If enabled, the underlying request to DynamoDB will include the + * configured parameter value and the low-level response from DynamoDB will + * include the amount of capacity consumed by the query. Currently, the + * consumed capacity is only exposed through the DynamoDBMapper when you + * call {@code DynamoDBMapper.queryPage}, not {@code DynamoDBMapper.query}. + * + * @param returnConsumedCapacity A value that if set to TOTAL, the response includes + * ConsumedCapacity data for tables and indexes. If set to + * INDEXES, the response includes ConsumedCapacity + * for indexes. If set to NONE (the default), + * ConsumedCapacity is not included in the response. + * + * @see com.amazonaws.services.dynamodbv2.model.ReturnConsumedCapacity + */ + public void setReturnConsumedCapacity(String returnConsumedCapacity) { + this.returnConsumedCapacity = returnConsumedCapacity; + } + + /** + * A value that if set to TOTAL, the response includes + * ConsumedCapacity data for tables and indexes. If set to + * INDEXES, the response includes ConsumedCapacity + * for indexes. If set to NONE (the default), + * ConsumedCapacity is not included in the response. + *

+ * Returns a reference to this object so that method calls can be chained together. + *

+ * Constraints:
+ * Allowed Values: INDEXES, TOTAL, NONE + *

+ * If enabled, the underlying request to DynamoDB will include the + * configured parameter value and the low-level response from DynamoDB will + * include the amount of capacity consumed by the query. Currently, the + * consumed capacity is only exposed through the DynamoDBMapper when you + * call {@code DynamoDBMapper.queryPage}, not {@code DynamoDBMapper.query}. + * + * @param returnConsumedCapacity A value that if set to TOTAL, the response includes + * ConsumedCapacity data for tables and indexes. If set to + * INDEXES, the response includes ConsumedCapacity + * for indexes. If set to NONE (the default), + * ConsumedCapacity is not included in the response. + * + * @return A reference to this updated object so that method calls can be chained + * together. + * + * @see com.amazonaws.services.dynamodbv2.model.ReturnConsumedCapacity + */ + public DynamoDBQueryExpression withReturnConsumedCapacity(String returnConsumedCapacity) { + this.returnConsumedCapacity = returnConsumedCapacity; + return this; + } + + /** + * A value that if set to TOTAL, the response includes + * ConsumedCapacity data for tables and indexes. If set to + * INDEXES, the response includes ConsumedCapacity + * for indexes. If set to NONE (the default), + * ConsumedCapacity is not included in the response. + *

+ * Constraints:
+ * Allowed Values: INDEXES, TOTAL, NONE + *

+ * If enabled, the underlying request to DynamoDB will include the + * configured parameter value and the low-level response from DynamoDB will + * include the amount of capacity consumed by the query. Currently, the + * consumed capacity is only exposed through the DynamoDBMapper when you + * call {@code DynamoDBMapper.queryPage}, not {@code DynamoDBMapper.query}. + * + * @param returnConsumedCapacity A value that if set to TOTAL, the response includes + * ConsumedCapacity data for tables and indexes. If set to + * INDEXES, the response includes ConsumedCapacity + * for indexes. If set to NONE (the default), + * ConsumedCapacity is not included in the response. + * + * @see com.amazonaws.services.dynamodbv2.model.ReturnConsumedCapacity + */ + public void setReturnConsumedCapacity(ReturnConsumedCapacity returnConsumedCapacity) { + this.returnConsumedCapacity = returnConsumedCapacity.toString(); + } + + /** + * A value that if set to TOTAL, the response includes + * ConsumedCapacity data for tables and indexes. If set to + * INDEXES, the response includes ConsumedCapacity + * for indexes. If set to NONE (the default), + * ConsumedCapacity is not included in the response. + *

+ * Returns a reference to this object so that method calls can be chained together. + *

+ * Constraints:
+ * Allowed Values: INDEXES, TOTAL, NONE + *

+ * If enabled, the underlying request to DynamoDB will include the + * configured parameter value and the low-level response from DynamoDB will + * include the amount of capacity consumed by the query. Currently, the + * consumed capacity is only exposed through the DynamoDBMapper when you + * call {@code DynamoDBMapper.queryPage}, not {@code DynamoDBMapper.query}. + * + * @param returnConsumedCapacity A value that if set to TOTAL, the response includes + * ConsumedCapacity data for tables and indexes. If set to + * INDEXES, the response includes ConsumedCapacity + * for indexes. If set to NONE (the default), + * ConsumedCapacity is not included in the response. + * + * @return A reference to this updated object so that method calls can be chained + * together. + * + * @see com.amazonaws.services.dynamodbv2.model.ReturnConsumedCapacity + */ + public DynamoDBQueryExpression withReturnConsumedCapacity(ReturnConsumedCapacity returnConsumedCapacity) { + this.returnConsumedCapacity = returnConsumedCapacity.toString(); + return this; + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBRangeKey.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBRangeKey.java new file mode 100644 index 000000000000..0433c0591ba7 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBRangeKey.java @@ -0,0 +1,43 @@ +/* + * 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.mapper.dynamodb; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation for marking a property in a class as the range key for a DynamoDB + * table. Applied to the getter method or the class field for the range key + * property. If the annotation is applied directly to the class field, the + * corresponding getter and setter must be declared in the same class. + *

+ * This annotation is required for tables that use a range key. + */ +@DynamoDB +@DynamoDBKeyed(software.amazon.awssdk.services.dynamodb.model.KeyType.RANGE) +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.METHOD}) +public @interface DynamoDBRangeKey { + + /** + * Optional parameter when the name of the attribute as stored in DynamoDB + * should differ from the name used by the getter / setter. + */ + String attributeName() default ""; + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBSaveExpression.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBSaveExpression.java new file mode 100644 index 000000000000..d6c0cac080ba --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBSaveExpression.java @@ -0,0 +1,133 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import java.util.HashMap; +import java.util.Map; + +import software.amazon.awssdk.services.dynamodb.model.ConditionalOperator; +import software.amazon.awssdk.services.dynamodb.model.ExpectedAttributeValue; + +/** + * Enables adding options to a save operation. + * For example, you may want to save only if an attribute has a particular value. + * @see DynamoDBMapper#save(Object, DynamoDBSaveExpression) + */ +@SdkPublicApi +public final class DynamoDBSaveExpression { + + /** Optional expected attributes */ + private Map expectedAttributes; + + /** + * The logical operator on the expected value conditions of this save + * operation. + */ + private String conditionalOperator; + + /** + * Gets the map of attribute names to expected attribute values to check on save. + * + * @return The map of attribute names to expected attribute value conditions to check on save + */ + public Map getExpected() { + return expectedAttributes; + } + + /** + * Sets the expected condition to the map of attribute names to expected attribute values given. + * + * @param expectedAttributes + * The map of attribute names to expected attribute value conditions to check on save + */ + public void setExpected(Map expectedAttributes) { + this.expectedAttributes = expectedAttributes; + } + + /** + * Sets the expected condition to the map of attribute names to expected + * attribute values given and returns a pointer to this object for + * method-chaining. + * + * @param expectedAttributes + * The map of attribute names to expected attribute value + * conditions to check on save + */ + public DynamoDBSaveExpression withExpected(Map expectedAttributes) { + setExpected(expectedAttributes); + return this; + } + + /** + * Adds one entry to the expected conditions and returns a pointer to this + * object for method-chaining. + * + * @param attributeName + * The name of the attribute. + * @param expected + * The expected attribute value. + */ + public DynamoDBSaveExpression withExpectedEntry(String attributeName, ExpectedAttributeValue expected) { + if (expectedAttributes == null) { + expectedAttributes = new HashMap(); + } + expectedAttributes.put(attributeName, expected); + return this; + } + + /** + * Returns the logical operator on the expected value conditions of this save + * operation. + */ + public String getConditionalOperator() { + return conditionalOperator; + } + + /** + * Sets the logical operator on the expected value conditions of this save + * operation. + */ + public void setConditionalOperator(String conditionalOperator) { + this.conditionalOperator = conditionalOperator; + } + + /** + * Sets the logical operator on the expected value conditions of this save + * operation and returns a pointer to this object for method-chaining. + */ + public DynamoDBSaveExpression withConditionalOperator(String conditionalOperator) { + setConditionalOperator(conditionalOperator); + return this; + } + + /** + * Sets the logical operator on the expected value conditions of this save + * operation. + */ + public void setConditionalOperator(ConditionalOperator conditionalOperator) { + setConditionalOperator(conditionalOperator.toString()); + } + + /** + * Sets the logical operator on the expected value conditions of this save + * operation and returns a pointer to this object for method-chaining. + */ + public DynamoDBSaveExpression withConditionalOperator(ConditionalOperator conditionalOperator) { + return withConditionalOperator(conditionalOperator.toString()); + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBScalarAttribute.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBScalarAttribute.java new file mode 100644 index 000000000000..a4c116db9f23 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBScalarAttribute.java @@ -0,0 +1,46 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @Deprecated - Replaced by {@link DynamoDBTyped} + */ +@Deprecated +@DynamoDB +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.METHOD}) +public @interface DynamoDBScalarAttribute { + + /** + * Optional parameter when the name of the attribute as stored in DynamoDB + * should differ from the name used by the getter / setter. + */ + String attributeName() default ""; + + /** + * The scalar attirbute type. + * @see software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType + */ + ScalarAttributeType type(); + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBScanExpression.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBScanExpression.java new file mode 100644 index 000000000000..a209af77ff3e --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBScanExpression.java @@ -0,0 +1,1015 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import java.util.HashMap; +import java.util.Map; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.ComparisonOperator; +import software.amazon.awssdk.services.dynamodb.model.Condition; +import software.amazon.awssdk.services.dynamodb.model.ConditionalOperator; +import software.amazon.awssdk.services.dynamodb.model.ReturnConsumedCapacity; +import software.amazon.awssdk.services.dynamodb.model.ScanRequest; +import software.amazon.awssdk.services.dynamodb.model.Select; + +/** + * Options for filtering results from a scan operation. For example, callers can + * specify filter conditions so that only objects whose attributes match + * different conditions are returned (see {@link ComparisonOperator} for more + * information on the available comparison types). + * + * @see DynamoDBMapper#scan(Class, DynamoDBScanExpression) + */ +@SdkPublicApi +public final class DynamoDBScanExpression { + + /** Optional filter to limit the results of the scan. */ + private Map scanFilter; + + /** The exclusive start key for this scan. */ + private Map exclusiveStartKey; + + /** The limit of items to scan during this scan. */ + private Integer limit; + + /** + * The total number of segments into which the scan will be divided. + * Only required for parallel scan operation. + */ + private Integer totalSegments; + + /** + * The ID (zero-based) of the segment to be scanned. + * Only required for parallel scan operation. + */ + private Integer segment; + + /** + * The logical operator on the filter conditions of this scan. + */ + private String conditionalOperator; + + /** + * Evaluates the scan results and returns only the desired values.

The + * condition you specify is applied to the items scanned; any items that + * do not match the expression are not returned. + */ + private String filterExpression; + + /** + * One or more substitution variables for simplifying complex + * expressions. The following are some use cases for an + * ExpressionAttributeName:

  • Shorten an attribute name that + * is very long or unwieldy in an expression.

  • Create a + * placeholder for repeating occurrences of an attribute name in an + * expression.

  • Prevent special characters in an attribute + * name from being misinterpreted in an expression.

Use + * the # character in an expression to dereference an attribute + * name. For example, consider the following expression: + *

  • order.customerInfo.LastName = "Smith" OR + * order.customerInfo.LastName = "Jones"

Now suppose + * that you specified the following for ExpressionAttributeNames: + *

  • {"n":"order.customerInfo.LastName"}

+ *

The expression can now be simplified as follows: + *

  • #n = "Smith" OR #n = "Jones"

+ */ + private java.util.Map expressionAttributeNames; + + /** + * One or more values that can be substituted in an expression.

Use + * the : character in an expression to dereference an attribute + * value. For example, consider the following expression: + *

  • ProductStatus IN + * ("Available","Backordered","Discontinued")

Now + * suppose that you specified the following for + * ExpressionAttributeValues:

  • { + * "a":{"S":"Available"}, "b":{"S":"Backordered"}, + * "d":{"S":"Discontinued"} }

The expression can now + * be simplified as follows:

  • ProductStatus IN + * (:a,:b,:c)

+ */ + private java.util.Map expressionAttributeValues; + + /** + * The attributes to be returned in the result. You can retrieve all item + * attributes, specific item attributes, the count of matching items, or + * in the case of an index, some or all of the attributes projected into + * the index.
  • ALL_ATTRIBUTES - Returns all of + * the item attributes from the specified table or index. If you query a + * local secondary index, then for each matching item in the index + * DynamoDB will fetch the entire item from the parent table. If the + * index is configured to project all item attributes, then all of the + * data can be obtained from the local secondary index, and no fetching + * is required.

  • ALL_PROJECTED_ATTRIBUTES - + * Allowed only when querying an index. Retrieves all attributes that + * have been projected into the index. If the index is configured to + * project all attributes, this return value is equivalent to specifying + * ALL_ATTRIBUTES.

  • COUNT - + * Returns the number of matching items, rather than the matching items + * themselves.

  • SPECIFIC_ATTRIBUTES - Returns + * only the attributes listed in AttributesToGet. This return + * value is equivalent to specifying AttributesToGet without + * specifying any value for Select.

    If you query a local + * secondary index and request only attributes that are projected into + * that index, the operation will read only the index and not the table. + * If any of the requested attributes are not projected into the local + * secondary index, DynamoDB will fetch each of these attributes from the + * parent table. This extra fetching incurs additional throughput cost + * and latency.

    If you query a global secondary index, you can only + * request attributes that are projected into the index. Global secondary + * index queries cannot fetch attributes from the parent table.

  • + *

If neither Select nor AttributesToGet are + * specified, DynamoDB defaults to ALL_ATTRIBUTES when + * accessing a table, and ALL_PROJECTED_ATTRIBUTES when + * accessing an index. You cannot use both Select and + * AttributesToGet together in a single request, unless the value + * for Select is SPECIFIC_ATTRIBUTES. (This usage is + * equivalent to specifying AttributesToGet without any value for + * Select.) + *

+ * Constraints:
+ * Allowed Values: ALL_ATTRIBUTES, ALL_PROJECTED_ATTRIBUTES, SPECIFIC_ATTRIBUTES, COUNT + */ + private String select; + + /** + * A string that identifies one or more attributes to retrieve from the + * table. These attributes can include scalars, sets, or elements of a + * JSON document. The attributes in the expression must be separated by + * commas.

If no attribute names are specified, then all attributes + * will be returned. If any of the requested attributes are not found, + * they will not appear in the result.

For more information, go to Accessing + * Item Attributes in the Amazon DynamoDB Developer Guide. + */ + private String projectionExpression; + + /** + * A value that if set to TOTAL, the response includes + * ConsumedCapacity data for tables and indexes. If set to + * INDEXES, the response includes ConsumedCapacity + * for indexes. If set to NONE (the default), + * ConsumedCapacity is not included in the response. + *

+ * Constraints:
+ * Allowed Values: INDEXES, TOTAL, NONE + *

+ * If enabled, the underlying request to DynamoDB will include the + * configured parameter value and the low-level response from DynamoDB will + * include the amount of capacity consumed by the scan. Currently, the + * consumed capacity is only exposed through the DynamoDBMapper when you + * call {@code DynamoDBMapper.scanPage}, not {@code DynamoDBMapper.scan}. + */ + private String returnConsumedCapacity; + + /** + * Optional index name that can be specified for the scan operation. + */ + private String indexName; + + private Boolean consistentRead; + + /** + * Returns the name of the index to be used by this scan; or null if there + * is none. + */ + public String getIndexName() { + return indexName; + } + + /** + * Sets the name of the index to be used by this scan. + */ + public void setIndexName(String indexName) { + this.indexName = indexName; + } + + /** + * Sets the name of the index to be used by this scan. + *

+ * Returns a pointer to this object for method-chaining. + */ + public DynamoDBScanExpression withIndexName(String indexName) { + setIndexName(indexName); + return this; + } + + /** + * Returns the scan filter as a map of attribute names to conditions. + * + * @return The scan filter as a map of attribute names to conditions. + */ + public Map getScanFilter() { + return scanFilter; + } + + /** + * Sets the scan filter to the map of attribute names to conditions given. + * + * @param scanFilter + * The map of attribute names to conditions to use when filtering + * scan results. + */ + public void setScanFilter(Map scanFilter) { + this.scanFilter = scanFilter; + } + + /** + * Sets the scan filter to the map of attribute names to conditions given + * and returns a pointer to this object for method-chaining. + * + * @param scanFilter + * The map of attribute names to conditions to use when filtering + * scan results. + */ + public DynamoDBScanExpression withScanFilter(Map scanFilter) { + setScanFilter(scanFilter); + return this; + } + + /** + * Adds a new filter condition to the current scan filter. + * + * @param attributeName + * The name of the attribute on which the specified condition + * operates. + * @param condition + * The condition which describes how the specified attribute is + * compared and if a row of data is included in the results + * returned by the scan operation. + */ + public void addFilterCondition(String attributeName, Condition condition) { + if ( scanFilter == null ) + scanFilter = new HashMap(); + + scanFilter.put(attributeName, condition); + } + + /** + * Adds a new filter condition to the current scan filter and returns a + * pointer to this object for method-chaining. + * + * @param attributeName + * The name of the attribute on which the specified condition + * operates. + * @param condition + * The condition which describes how the specified attribute is + * compared and if a row of data is included in the results + * returned by the scan operation. + */ + public DynamoDBScanExpression withFilterConditionEntry(String attributeName, Condition condition) { + if ( scanFilter == null ) + scanFilter = new HashMap(); + + scanFilter.put(attributeName, condition); + return this; + } + + + /** + * Returns the exclusive start key for this scan. + */ + public Map getExclusiveStartKey() { + return exclusiveStartKey; + } + + /** + * Sets the exclusive start key for this scan. + */ + public void setExclusiveStartKey(Map exclusiveStartKey) { + this.exclusiveStartKey = exclusiveStartKey; + } + + /** + * Sets the exclusive start key for this scan and returns a pointer to this + * object for method-chaining. + */ + public DynamoDBScanExpression withExclusiveStartKey(Map exclusiveStartKey) { + this.exclusiveStartKey = exclusiveStartKey; + return this; + } + + /** + * Returns the limit of items to scan during this scan. + *

+ * Use with caution. Please note that this is not the same as the + * number of items to return from the scan operation -- the operation will + * cease and return as soon as this many items are scanned, even if no + * matching results are found. Furthermore, {@link PaginatedScanList} will + * execute as many scan operations as necessary until it either reaches the + * end of the result set as indicated by DynamoDB or enough elements are + * available to fulfill the list operation (e.g. iteration). Therefore, + * except when scanning without a scan filter, it's usually bad practice to + * set a low limit, since doing so will often generate the same amount of + * traffic to DynamoDB but with a greater number of round trips and + * therefore more overall latency. + */ + public Integer getLimit() { + return limit; + } + + /** + * Sets the limit of items to scan during this scan. Please note that this + * is not the same as the number of items to return from the scan + * operation -- the operation will cease and return as soon as this many + * items are scanned, even if no matching results are found. + * + * @see DynamoDBScanExpression#getLimit() + */ + public void setLimit(Integer limit) { + this.limit = limit; + } + + /** + * Sets the limit of items to scan and returns a pointer to this object for + * method-chaining. Please note that this is not the same as the + * number of items to return from the scan operation -- the operation will + * cease and return as soon as this many items are scanned, even if no + * matching results are found. + * + * @see DynamoDBScanExpression#getLimit() + */ + public DynamoDBScanExpression withLimit(Integer limit) { + this.limit = limit; + return this; + } + + /** + * Returns the total number of segments into which the scan will be divided. + */ + public Integer getTotalSegments() { + return totalSegments; + } + + /** + * Sets the total number of segments into which the scan will be divided. + */ + public void setTotalSegments(Integer totalSegments) { + this.totalSegments = totalSegments; + } + + /** + * Sets the total number of segments into which the scan will be divided and + * returns a pointer to this object for method-chaining. + */ + public DynamoDBScanExpression withTotalSegments(Integer totalSegments) { + setTotalSegments(totalSegments); + return this; + } + + /** + * Returns the ID of the segment to be scanned. + */ + public Integer getSegment() { + return segment; + } + + /** + * Sets the ID of the segment to be scanned. + */ + public void setSegment(Integer segment) { + this.segment = segment; + } + + /** + * Sets the ID of the segment to be scanned and returns a pointer to this + * object for method-chaining. + */ + public DynamoDBScanExpression withSegment(Integer segment) { + setSegment(segment); + return this; + } + + /** + * Returns the logical operator on the filter conditions of this scan. + */ + public String getConditionalOperator() { + return conditionalOperator; + } + + /** + * Sets the logical operator on the filter conditions of this scan. + */ + public void setConditionalOperator(String conditionalOperator) { + this.conditionalOperator = conditionalOperator; + } + + /** + * Sets the logical operator on the filter conditions of this scan and + * returns a pointer to this object for method-chaining. + */ + public DynamoDBScanExpression withConditionalOperator(String conditionalOperator) { + setConditionalOperator(conditionalOperator); + return this; + } + + /** + * Sets the logical operator on the filter conditions of this scan. + */ + public void setConditionalOperator(ConditionalOperator conditionalOperator) { + setConditionalOperator(conditionalOperator.toString()); + } + + /** + * Sets the logical operator on the filter conditions of this scan and + * returns a pointer to this object for method-chaining. + */ + public DynamoDBScanExpression withConditionalOperator(ConditionalOperator conditionalOperator) { + return withConditionalOperator(conditionalOperator.toString()); + } + + /** + * Evaluates the query results and returns only the desired values. + *

+ * The condition you specify is applied to the items queried; any items that + * do not match the expression are not returned. + * + * @return Evaluates the query results and returns only the desired values. + *

+ * The condition you specify is applied to the items queried; any + * items that do not match the expression are not returned. + * @see ScanRequest#getFilterExpression() + */ + public String getFilterExpression() { + return filterExpression; + } + + /** + * Evaluates the query results and returns only the desired values. + *

+ * The condition you specify is applied to the items queried; any items that + * do not match the expression are not returned. + * + * @param filterExpression + * Evaluates the query results and returns only the desired + * values. + *

+ * The condition you specify is applied to the items queried; any + * items that do not match the expression are not returned. + * @see ScanRequest#setFilterExpression(String) + */ + public void setFilterExpression(String filterExpression) { + this.filterExpression = filterExpression; + } + + /** + * Evaluates the query results and returns only the desired values. + *

+ * The condition you specify is applied to the items queried; any items that + * do not match the expression are not returned. + *

+ * Returns a reference to this object so that method calls can be chained + * together. + * + * @param filterExpression + * Evaluates the query results and returns only the desired + * values. + *

+ * The condition you specify is applied to the items queried; any + * items that do not match the expression are not returned. + * + * @return A reference to this updated object so that method calls can be + * chained together. + * @see ScanRequest#withFilterExpression(String) + */ + public DynamoDBScanExpression withFilterExpression(String filterExpression) { + this.filterExpression = filterExpression; + return this; + } + + /** + * One or more substitution variables for simplifying complex expressions. + * + * @return One or more substitution variables for simplifying complex + * expressions. + * @see scanRequest#getExpressionAttributeNames() + */ + public java.util.Map getExpressionAttributeNames() { + + return expressionAttributeNames; + } + + /** + * One or more substitution variables for simplifying complex expressions. + * + * @param expressionAttributeNames + * One or more substitution variables for simplifying complex + * expressions. + * @see ScanRequest#setExpressionAttributeNames(Map) + */ + public void setExpressionAttributeNames( + java.util.Map expressionAttributeNames) { + this.expressionAttributeNames = expressionAttributeNames; + } + + /** + * One or more substitution variables for simplifying complex expressions. + * + * @param expressionAttributeNames + * One or more substitution variables for simplifying complex + * expressions. + * + * @return A reference to this updated object so that method calls can be + * chained together. + * @see ScanRequest#withExpressionAttributeNames(Map) + */ + public DynamoDBScanExpression withExpressionAttributeNames( + java.util.Map expressionAttributeNames) { + setExpressionAttributeNames(expressionAttributeNames); + return this; + } + + /** + * One or more substitution variables for simplifying complex expressions. + * The method adds a new key-value pair into ExpressionAttributeNames + * parameter, and returns a reference to this object so that method calls + * can be chained together. + * + * @param key + * The key of the entry to be added into + * ExpressionAttributeNames. + * @param value + * The corresponding value of the entry to be added into + * ExpressionAttributeNames. + * + * @see ScanRequest#addExpressionAttributeNamesEntry(String, String) + */ + public DynamoDBScanExpression addExpressionAttributeNamesEntry(String key, + String value) { + if (null == this.expressionAttributeNames) { + this.expressionAttributeNames = new java.util.HashMap(); + } + if (this.expressionAttributeNames.containsKey(key)) + throw new IllegalArgumentException("Duplicated keys (" + + key.toString() + ") are provided."); + this.expressionAttributeNames.put(key, value); + return this; + } + + /** + * Removes all the entries added into ExpressionAttributeNames. + *

+ * Returns a reference to this object so that method calls can be chained + * together. + */ + public DynamoDBScanExpression clearExpressionAttributeNamesEntries() { + this.expressionAttributeNames = null; + return this; + } + + /** + * One or more values that can be substituted in an expression. + * + * @return One or more values that can be substituted in an expression. + * + * @see ScanRequest#getExpressionAttributeValues() + */ + public java.util.Map getExpressionAttributeValues() { + + return expressionAttributeValues; + } + + /** + * One or more values that can be substituted in an expression. + * + * @param expressionAttributeValues + * One or more values that can be substituted in an expression. + * + * @see ScanRequest#setExpressionAttributeValues(Map) + */ + public void setExpressionAttributeValues( + java.util.Map expressionAttributeValues) { + this.expressionAttributeValues = expressionAttributeValues; + } + + /** + * One or more values that can be substituted in an expression. + * + * @param expressionAttributeValues + * One or more values that can be substituted in an expression. + * + * @return A reference to this updated object so that method calls can be + * chained together. + * @see ScanRequest#withExpressionAttributeValues(Map) + */ + public DynamoDBScanExpression withExpressionAttributeValues( + java.util.Map expressionAttributeValues) { + setExpressionAttributeValues(expressionAttributeValues); + return this; + } + + /** + * One or more values that can be substituted in an expression. The method + * adds a new key-value pair into ExpressionAttributeValues parameter, and + * returns a reference to this object so that method calls can be chained + * together. + * + * @param key + * The key of the entry to be added into + * ExpressionAttributeValues. + * @param value + * The corresponding value of the entry to be added into + * ExpressionAttributeValues. + */ + public DynamoDBScanExpression addExpressionAttributeValuesEntry(String key, + AttributeValue value) { + if (null == this.expressionAttributeValues) { + this.expressionAttributeValues = new java.util.HashMap(); + } + if (this.expressionAttributeValues.containsKey(key)) + throw new IllegalArgumentException("Duplicated keys (" + + key.toString() + ") are provided."); + this.expressionAttributeValues.put(key, value); + return this; + } + + /** + * Removes all the entries added into ExpressionAttributeValues. + *

+ * Returns a reference to this object so that method calls can be chained + * together. + */ + public DynamoDBScanExpression clearExpressionAttributeValuesEntries() { + this.expressionAttributeValues = null; + return this; + } + + /** + * The attributes to be returned in the result. You can retrieve all item + * attributes, specific item attributes, the count of matching items, or + * in the case of an index, some or all of the attributes projected into + * the index. + *

+ * Constraints:
+ * Allowed Values: ALL_ATTRIBUTES, ALL_PROJECTED_ATTRIBUTES, SPECIFIC_ATTRIBUTES, COUNT + * + * @return The attributes to be returned in the result. You can retrieve all item + * attributes, specific item attributes, the count of matching items, or + * in the case of an index, some or all of the attributes projected into + * the index. + * + * @see com.amazonaws.services.dynamodbv2.model.Select + */ + public String getSelect() { + return select; + } + + /** + * The attributes to be returned in the result. You can retrieve all item + * attributes, specific item attributes, the count of matching items, or + * in the case of an index, some or all of the attributes projected into + * the index. + *

+ * Constraints:
+ * Allowed Values: ALL_ATTRIBUTES, ALL_PROJECTED_ATTRIBUTES, SPECIFIC_ATTRIBUTES, COUNT + * + * @param select The attributes to be returned in the result. You can retrieve all item + * attributes, specific item attributes, the count of matching items, or + * in the case of an index, some or all of the attributes projected into + * the index. + * + * @see com.amazonaws.services.dynamodbv2.model.Select + */ + public void setSelect(String select) { + this.select = select; + } + + /** + * The attributes to be returned in the result. You can retrieve all item + * attributes, specific item attributes, the count of matching items, or + * in the case of an index, some or all of the attributes projected into + * the index. + *

+ * Returns a reference to this object so that method calls can be chained together. + *

+ * Constraints:
+ * Allowed Values: ALL_ATTRIBUTES, ALL_PROJECTED_ATTRIBUTES, SPECIFIC_ATTRIBUTES, COUNT + * + * @param select The attributes to be returned in the result. You can retrieve all item + * attributes, specific item attributes, the count of matching items, or + * in the case of an index, some or all of the attributes projected into + * the index. + * + * @return A reference to this updated object so that method calls can be chained + * together. + * + * @see com.amazonaws.services.dynamodbv2.model.Select + */ + public DynamoDBScanExpression withSelect(String select) { + this.select = select; + return this; + } + + /** + * The attributes to be returned in the result. You can retrieve all item + * attributes, specific item attributes, the count of matching items, or + * in the case of an index, some or all of the attributes projected into + * the index. + *

+ * Constraints:
+ * Allowed Values: ALL_ATTRIBUTES, ALL_PROJECTED_ATTRIBUTES, SPECIFIC_ATTRIBUTES, COUNT + * + * @param select The attributes to be returned in the result. You can retrieve all item + * attributes, specific item attributes, the count of matching items, or + * in the case of an index, some or all of the attributes projected into + * the index. + * + * @see com.amazonaws.services.dynamodbv2.model.Select + */ + public void setSelect(Select select) { + this.select = select.toString(); + } + + /** + * The attributes to be returned in the result. You can retrieve all item + * attributes, specific item attributes, the count of matching items, or + * in the case of an index, some or all of the attributes projected into + * the index. + *

+ * Returns a reference to this object so that method calls can be chained together. + *

+ * Constraints:
+ * Allowed Values: ALL_ATTRIBUTES, ALL_PROJECTED_ATTRIBUTES, SPECIFIC_ATTRIBUTES, COUNT + * + * @param select The attributes to be returned in the result. You can retrieve all item + * attributes, specific item attributes, the count of matching items, or + * in the case of an index, some or all of the attributes projected into + * the index. + * + * @return A reference to this updated object so that method calls can be chained + * together. + * + * @see com.amazonaws.services.dynamodbv2.model.Select + */ + public DynamoDBScanExpression withSelect(Select select) { + this.select = select.toString(); + return this; + } + + /** + * A string that identifies one or more attributes to retrieve from the + * table. These attributes can include scalars, sets, or elements of a + * JSON document. The attributes in the expression must be separated by + * commas.

If no attribute names are specified, then all attributes + * will be returned. If any of the requested attributes are not found, + * they will not appear in the result.

For more information, go to Accessing + * Item Attributes in the Amazon DynamoDB Developer Guide. + * + * @return A string that identifies one or more attributes to retrieve from the + * table. These attributes can include scalars, sets, or elements of a + * JSON document. The attributes in the expression must be separated by + * commas.

If no attribute names are specified, then all attributes + * will be returned. If any of the requested attributes are not found, + * they will not appear in the result.

For more information, go to Accessing + * Item Attributes in the Amazon DynamoDB Developer Guide. + */ + public String getProjectionExpression() { + return projectionExpression; + } + + /** + * A string that identifies one or more attributes to retrieve from the + * table. These attributes can include scalars, sets, or elements of a + * JSON document. The attributes in the expression must be separated by + * commas.

If no attribute names are specified, then all attributes + * will be returned. If any of the requested attributes are not found, + * they will not appear in the result.

For more information, go to Accessing + * Item Attributes in the Amazon DynamoDB Developer Guide. + * + * @param projectionExpression A string that identifies one or more attributes to retrieve from the + * table. These attributes can include scalars, sets, or elements of a + * JSON document. The attributes in the expression must be separated by + * commas.

If no attribute names are specified, then all attributes + * will be returned. If any of the requested attributes are not found, + * they will not appear in the result.

For more information, go to Accessing + * Item Attributes in the Amazon DynamoDB Developer Guide. + */ + public void setProjectionExpression(String projectionExpression) { + this.projectionExpression = projectionExpression; + } + + /** + * A string that identifies one or more attributes to retrieve from the + * table. These attributes can include scalars, sets, or elements of a + * JSON document. The attributes in the expression must be separated by + * commas.

If no attribute names are specified, then all attributes + * will be returned. If any of the requested attributes are not found, + * they will not appear in the result.

For more information, go to Accessing + * Item Attributes in the Amazon DynamoDB Developer Guide. + *

+ * Returns a reference to this object so that method calls can be chained together. + * + * @param projectionExpression A string that identifies one or more attributes to retrieve from the + * table. These attributes can include scalars, sets, or elements of a + * JSON document. The attributes in the expression must be separated by + * commas.

If no attribute names are specified, then all attributes + * will be returned. If any of the requested attributes are not found, + * they will not appear in the result.

For more information, go to Accessing + * Item Attributes in the Amazon DynamoDB Developer Guide. + * + * @return A reference to this updated object so that method calls can be chained + * together. + */ + public DynamoDBScanExpression withProjectionExpression(String projectionExpression) { + this.projectionExpression = projectionExpression; + return this; + } + + /** + * A value that if set to TOTAL, the response includes + * ConsumedCapacity data for tables and indexes. If set to + * INDEXES, the response includes ConsumedCapacity + * for indexes. If set to NONE (the default), + * ConsumedCapacity is not included in the response. + *

+ * Constraints:
+ * Allowed Values: INDEXES, TOTAL, NONE + *

+ * If enabled, the underlying request to DynamoDB will include the + * configured parameter value and the low-level response from DynamoDB will + * include the amount of capacity consumed by the scan. Currently, the + * consumed capacity is only exposed through the DynamoDBMapper when you + * call {@code DynamoDBMapper.scanPage}, not {@code DynamoDBMapper.scan}. + * + * @return A value that if set to TOTAL, the response includes + * ConsumedCapacity data for tables and indexes. If set to + * INDEXES, the response includes ConsumedCapacity + * for indexes. If set to NONE (the default), + * ConsumedCapacity is not included in the response. + * + * @see com.amazonaws.services.dynamodbv2.model.ReturnConsumedCapacity + */ + public String getReturnConsumedCapacity() { + return returnConsumedCapacity; + } + + /** + * A value that if set to TOTAL, the response includes + * ConsumedCapacity data for tables and indexes. If set to + * INDEXES, the response includes ConsumedCapacity + * for indexes. If set to NONE (the default), + * ConsumedCapacity is not included in the response. + *

+ * Constraints:
+ * Allowed Values: INDEXES, TOTAL, NONE + *

+ * If enabled, the underlying request to DynamoDB will include the + * configured parameter value and the low-level response from DynamoDB will + * include the amount of capacity consumed by the scan. Currently, the + * consumed capacity is only exposed through the DynamoDBMapper when you + * call {@code DynamoDBMapper.scanPage}, not {@code DynamoDBMapper.scan}. + * + * @param returnConsumedCapacity A value that if set to TOTAL, the response includes + * ConsumedCapacity data for tables and indexes. If set to + * INDEXES, the response includes ConsumedCapacity + * for indexes. If set to NONE (the default), + * ConsumedCapacity is not included in the response. + * + * @see com.amazonaws.services.dynamodbv2.model.ReturnConsumedCapacity + */ + public void setReturnConsumedCapacity(String returnConsumedCapacity) { + this.returnConsumedCapacity = returnConsumedCapacity; + } + + /** + * A value that if set to TOTAL, the response includes + * ConsumedCapacity data for tables and indexes. If set to + * INDEXES, the response includes ConsumedCapacity + * for indexes. If set to NONE (the default), + * ConsumedCapacity is not included in the response. + *

+ * Returns a reference to this object so that method calls can be chained together. + *

+ * Constraints:
+ * Allowed Values: INDEXES, TOTAL, NONE + *

+ * If enabled, the underlying request to DynamoDB will include the + * configured parameter value and the low-level response from DynamoDB will + * include the amount of capacity consumed by the scan. Currently, the + * consumed capacity is only exposed through the DynamoDBMapper when you + * call {@code DynamoDBMapper.scanPage}, not {@code DynamoDBMapper.scan}. + * + * @param returnConsumedCapacity A value that if set to TOTAL, the response includes + * ConsumedCapacity data for tables and indexes. If set to + * INDEXES, the response includes ConsumedCapacity + * for indexes. If set to NONE (the default), + * ConsumedCapacity is not included in the response. + * + * @return A reference to this updated object so that method calls can be chained + * together. + * + * @see com.amazonaws.services.dynamodbv2.model.ReturnConsumedCapacity + */ + public DynamoDBScanExpression withReturnConsumedCapacity(String returnConsumedCapacity) { + this.returnConsumedCapacity = returnConsumedCapacity; + return this; + } + + /** + * A value that if set to TOTAL, the response includes + * ConsumedCapacity data for tables and indexes. If set to + * INDEXES, the response includes ConsumedCapacity + * for indexes. If set to NONE (the default), + * ConsumedCapacity is not included in the response. + *

+ * Constraints:
+ * Allowed Values: INDEXES, TOTAL, NONE + *

+ * If enabled, the underlying request to DynamoDB will include the + * configured parameter value and the low-level response from DynamoDB will + * include the amount of capacity consumed by the scan. Currently, the + * consumed capacity is only exposed through the DynamoDBMapper when you + * call {@code DynamoDBMapper.scanPage}, not {@code DynamoDBMapper.scan}. + * + * @param returnConsumedCapacity A value that if set to TOTAL, the response includes + * ConsumedCapacity data for tables and indexes. If set to + * INDEXES, the response includes ConsumedCapacity + * for indexes. If set to NONE (the default), + * ConsumedCapacity is not included in the response. + * + * @see com.amazonaws.services.dynamodbv2.model.ReturnConsumedCapacity + */ + public void setReturnConsumedCapacity(ReturnConsumedCapacity returnConsumedCapacity) { + this.returnConsumedCapacity = returnConsumedCapacity.toString(); + } + + /** + * A value that if set to TOTAL, the response includes + * ConsumedCapacity data for tables and indexes. If set to + * INDEXES, the response includes ConsumedCapacity + * for indexes. If set to NONE (the default), + * ConsumedCapacity is not included in the response. + *

+ * Returns a reference to this object so that method calls can be chained together. + *

+ * Constraints:
+ * Allowed Values: INDEXES, TOTAL, NONE + *

+ * If enabled, the underlying request to DynamoDB will include the + * configured parameter value and the low-level response from DynamoDB will + * include the amount of capacity consumed by the scan. Currently, the + * consumed capacity is only exposed through the DynamoDBMapper when you + * call {@code DynamoDBMapper.scanPage}, not {@code DynamoDBMapper.scan}. + * + * @param returnConsumedCapacity A value that if set to TOTAL, the response includes + * ConsumedCapacity data for tables and indexes. If set to + * INDEXES, the response includes ConsumedCapacity + * for indexes. If set to NONE (the default), + * ConsumedCapacity is not included in the response. + * + * @return A reference to this updated object so that method calls can be chained + * together. + * + * @see com.amazonaws.services.dynamodbv2.model.ReturnConsumedCapacity + */ + public DynamoDBScanExpression withReturnConsumedCapacity(ReturnConsumedCapacity returnConsumedCapacity) { + this.returnConsumedCapacity = returnConsumedCapacity.toString(); + return this; + } + + /** + * Returns whether this scan uses consistent reads. + * + * @see ScanRequest#isConsistentRead() + */ + public Boolean isConsistentRead() { + return consistentRead; + } + + /** + * Sets whether this scan uses consistent reads. + */ + public void setConsistentRead(Boolean consistentRead) { + this.consistentRead = consistentRead; + } + + /** + * Sets whether this scan uses consistent reads and returns a reference + * to this object for method chaining. + */ + public DynamoDBScanExpression withConsistentRead(Boolean consistentRead) { + this.consistentRead = consistentRead; + return this; + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBTable.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBTable.java new file mode 100644 index 000000000000..147377dcc177 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBTable.java @@ -0,0 +1,46 @@ +/* + * 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.mapper.dynamodb; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.TableNameOverride; + + +/** + * Annotation to mark a class as a DynamoDB table. + *

+ * This annotation is inherited by subclasses, and can be overridden by them as + * well. + * + * @see TableNameOverride + */ +@DynamoDB +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Inherited +public @interface DynamoDBTable { + + /** + * The name of the table to use for this class. + */ + String tableName(); + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBTableMapper.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBTableMapper.java new file mode 100644 index 000000000000..08969f76bdc6 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBTableMapper.java @@ -0,0 +1,530 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBDeleteExpression; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBSaveExpression; +import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException; +import software.amazon.awssdk.services.dynamodb.model.ExpectedAttributeValue; +import software.amazon.awssdk.services.dynamodb.model.ProvisionedThroughput; +import software.amazon.awssdk.services.dynamodb.model.ResourceInUseException; +import software.amazon.awssdk.services.dynamodb.model.ResourceNotFoundException; +import software.amazon.awssdk.services.dynamodb.model.TableDescription; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import software.amazon.awssdk.utils.Logger; + +/** + * A wrapper for {@code DynamoDBMapper} which operates only on a specified + * class/table. All calls are forwarded to the underlying + * {@code DynamoDBMapper} which was used to create this table mapper. + * + * A minimal example using get annotations, + *

+ * @DynamoDBTable(tableName="TestTable")
+ * public class TestClass {
+ *     private Long key;
+ *     private String rangeKey;
+ *     private Double amount;
+ *     private Long version;
+ *
+ *     @DynamoDBHashKey
+ *     public Long getKey() { return key; }
+ *     public void setKey(Long key) { this.key = key; }
+ *
+ *     @DynamoDBRangeKey
+ *     public String getRangeKey() { return rangeKey; }
+ *     public void setRangeKey(String rangeKey) { this.rangeKey = rangeKey; }
+ *
+ *     @DynamoDBAttribute(attributeName="amount")
+ *     public Double getAmount() { return amount; }
+ *     public void setAmount(Double amount) { this.amount = amount; }
+ *
+ *     @DynamoDBVersionAttribute
+ *     public Long getVersion() { return version; }
+ *     public void setVersion(Long version) { this.version = version; }
+ * }
+ * 
+ * + * Initialize the DynamoDB mapper, + *
+ * AmazonDynamoDB dbClient = new AmazonDynamoDBClient();
+ * DynamoDBMapper dbMapper = new DynamoDBMapper(dbClient);
+ * 
+ * + * Then, create a new table mapper with hash and range key, + *
+ * DynamoDBTableMapper<TestClass,Long,String> mapper = dbMapper.newTableMapper(TestClass.class);
+ * 
+ * + * Or, if the table does not have a range key, + *
+ * DynamoDBTableMapper<TestClass,Long,?> table = dbMapper.newTableMapper(TestClass.class);
+ * 
+ * + * If you don't have your DynamoDB table set up yet, you can use, + *
+ * table.createTableIfNotExists(new ProvisionedThroughput(25L, 25L));
+ * 
+ * + * Save instances of annotated classes and retrieve them, + *
+ * TestClass object = new TestClass();
+ * object.setKey(1234L);
+ * object.setRangeKey("ABCD");
+ * object.setAmount(101D);
+ *
+ * try {
+ *     table.saveIfNotExists(object);
+ * } catch (ConditionalCheckFailedException e) {
+ *     // handle already existing
+ * }
+ * 
+ * + * Execute a query operation, + *
+ * int limit = 10;
+ * List<TestClass> objects = new ArrayList<TestClass>(limit);
+ *
+ * DynamoDBQueryExpression<TestClass> query = new DynamoDBQueryExpression()
+ *     .withRangeKeyCondition(table.rangeKey().name(), table.rangeKey().ge("ABAA"))
+ *     .withQueryFilterEntry("amount", table.field("amount").gt(100D))
+ *     .withHashKeyValues(1234L)
+ *     .withConsistentReads(true);
+ *
+ * QueryResultPage<TestClass> results = new QueryResultPage<TestClass>();
+ *
+ * do {
+ *     if (results.getLastEvaluatedKey() != null) {
+ *         query.setExclusiveStartKey(results.getLastEvaluatedKey());
+ *     }
+ *     query.setLimit(limit - objects.size());
+ *     results = mapper.query(query);
+ *     for (TestClass object : results.getResults()) {
+ *         objects.add(object);
+ *     }
+ * } while (results.getLastEvaluatedKey() != null && objects.size() < limit)
+ * 
+ * + * @param The object type which this mapper operates. + * @param The hash key value type. + * @param The range key value type; use ? if no range key. + * + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper + * @see com.amazonaws.services.dynamodbv2.AmazonDynamoDB + */ +@SdkPublicApi +public final class DynamoDBTableMapper { + + private static final Logger LOG = Logger.loggerFor(DynamoDBTableMapper.class); + + private final DynamoDBMapperTableModel model; + private final DynamoDBMapperFieldModel hk; + private final DynamoDBMapperFieldModel rk; + private final DynamoDBMapperConfig config; + private final DynamoDBMapper mapper; + private final DynamoDbClient db; + + /** + * Constructs a new table mapper for the given class. + * @param model The field model factory. + * @param mapper The DynamoDB mapper. + * @param db The service object to use for all service calls. + */ + protected DynamoDBTableMapper(DynamoDbClient db, DynamoDBMapper mapper, final DynamoDBMapperConfig config, final DynamoDBMapperTableModel model) { + this.rk = model.rangeKeyIfExists(); + this.hk = model.hashKey(); + this.model = model; + this.config = config; + this.mapper = mapper; + this.db = db; + } + + /** + * Gets the field model for a given attribute. + * @param The field model's value type. + * @param attributeName The attribute name. + * @return The field model. + */ + public DynamoDBMapperFieldModel field(String attributeName) { + return this.model.field(attributeName); + } + + /** + * Gets the hash key field model for the specified type. + * @param The hash key type. + * @return The hash key field model. + * @throws DynamoDBMappingException If the hash key is not present. + */ + public DynamoDBMapperFieldModel hashKey() { + return this.model.hashKey(); + } + + /** + * Gets the range key field model for the specified type. + * @param The range key type. + * @return The range key field model. + * @throws DynamoDBMappingException If the range key is not present. + */ + public DynamoDBMapperFieldModel rangeKey() { + return this.model.rangeKey(); + } + + /** + * Retrieves multiple items from the table using their primary keys. + * @param itemsToGet The items to get. + * @return The list of objects. + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper#batchLoad + */ + public List batchLoad(Iterable itemsToGet) { + Map> results = mapper.batchLoad(itemsToGet); + if (results.isEmpty()) { + return Collections.emptyList(); + } + return (List)results.get(mapper.getTableName(model.targetType(), config)); + } + + /** + * Saves the objects given using one or more calls to the batchWriteItem API. + * @param objectsToSave The objects to save. + * @return The list of failed batches. + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper#batchSave + */ + public List batchSave(Iterable objectsToSave) { + return mapper.batchWrite(objectsToSave, (Iterable)Collections.emptyList()); + } + + /** + * Deletes the objects given using one or more calls to the batchWtiteItem API. + * @param objectsToDelete The objects to delete. + * @return The list of failed batches. + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper#batchDelete + */ + public List batchDelete(Iterable objectsToDelete) { + return mapper.batchWrite((Iterable)Collections.emptyList(), objectsToDelete); + } + + /** + * Saves and deletes the objects given using one or more calls to the + * batchWriteItem API. + * @param objectsToWrite The objects to write. + * @param objectsToDelete The objects to delete. + * @return The list of failed batches. + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper#batchWrite + */ + public List batchWrite(Iterable objectsToWrite, Iterable objectsToDelete) { + return mapper.batchWrite(objectsToWrite, objectsToDelete); + } + + /** + * Transactionally writes objects specified by transactionWriteRequest by calling + * {@link software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper#transactionWrite(TransactionWriteRequest)} API. + * @param transactionWriteRequest List of objects to write + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper#transactionWrite(TransactionWriteRequest) + */ + public void transactionWrite(TransactionWriteRequest transactionWriteRequest) { + for (TransactionWriteRequest.TransactionWriteOperation transactionWriteOperation : transactionWriteRequest.getTransactionWriteOperations()) { + if (!model.targetType().equals(transactionWriteOperation.getObject().getClass())) { + throw new DynamoDBMappingException("Input object is of the classType: " + transactionWriteOperation.getObject().getClass() + + " but tableMapper is declared with classType: " + model.targetType()); + } + } + mapper.transactionWrite(transactionWriteRequest); + } + + /** + * Transactionally loads objects specified by transactionLoadRequest by calling + * {@link software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper#transactionLoad(TransactionLoadRequest)} API. + * @param transactionLoadRequest List of objects to load + * @return List of objects + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper#transactionLoad(TransactionLoadRequest) + */ + public List transactionLoad(TransactionLoadRequest transactionLoadRequest) { + for (Object object : transactionLoadRequest.getObjectsToLoad() ) { + if (!model.targetType().equals(object.getClass())) { + throw new DynamoDBMappingException("Input object is of the classType: " + object.getClass() + " but tableMapper is declared with classType: " + model.targetType()); + } + } + return mapper.transactionLoad(transactionLoadRequest); + } + + /** + * Loads an object with the hash key given. + * @param hashKey The hash key value. + * @return The object. + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper#load + */ + public T load(H hashKey) { + return mapper.load(model.targetType(), hashKey); + } + + /** + * Loads an object with the hash and range key. + * @param hashKey The hash key value. + * @param rangeKey The range key value. + * @return The object. + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper#load + */ + public T load(H hashKey, R rangeKey) { + return mapper.load(model.targetType(), hashKey, rangeKey); + } + + /** + * Saves the object given into DynamoDB. + * @param object The object to save. + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper#save + */ + public void save(T object) { + mapper.save(object); + } + + /** + * Saves the object given into DynamoDB using the specified saveExpression. + * @param object The object to save. + * @param saveExpression The save expression. + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper#save + */ + public void save(T object, DynamoDBSaveExpression saveExpression) { + mapper.save(object, saveExpression); + } + + /** + * Saves the object given into DynamoDB with the condition that the hash + * and if applicable, the range key, does not already exist. + * @param object The object to create. + * @throws ConditionalCheckFailedException If the object exists. + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper#save + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBSaveExpression + * @see com.amazonaws.services.dynamodbv2.model.ExpectedAttributeValue + */ + public void saveIfNotExists(T object) throws ConditionalCheckFailedException { + DynamoDBSaveExpression saveExpression = new DynamoDBSaveExpression(); + for (DynamoDBMapperFieldModel key : model.keys()) { + saveExpression.withExpectedEntry(key.name(), ExpectedAttributeValue.builder() + .exists(false).build()); + } + mapper.save(object, saveExpression); + } + + /** + * Saves the object given into DynamoDB with the condition that the hash + * and, if applicable, the range key, already exist. + * @param object The object to update. + * @throws ConditionalCheckFailedException If the object does not exist. + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper#save + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBSaveExpression + * @see com.amazonaws.services.dynamodbv2.model.ExpectedAttributeValue + */ + public void saveIfExists(T object) throws ConditionalCheckFailedException { + DynamoDBSaveExpression saveExpression = new DynamoDBSaveExpression(); + for (DynamoDBMapperFieldModel key : model.keys()) { + saveExpression.withExpectedEntry(key.name(), ExpectedAttributeValue.builder() + .exists(true).value(key.convert(key.get(object))).build()); + } + mapper.save(object, saveExpression); + } + + /** + * Deletes the given object from its DynamoDB table. + * @param object The object to delete. + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper#delete + */ + public final void delete(final T object) { + mapper.delete(object); + } + + /** + * Deletes the given object from its DynamoDB table using the specified + * deleteExpression. + * @param object The object to delete. + * @param deleteExpression The delete expression. + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper#delete + */ + public final void delete(final T object, final DynamoDBDeleteExpression deleteExpression) { + mapper.delete(object, deleteExpression); + } + + /** + * Deletes the given object from its DynamoDB table with the condition that + * the hash and, if applicable, the range key, already exist. + * @param object The object to delete. + * @throws ConditionalCheckFailedException If the object does not exist. + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper#delete + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBDeleteExpression + * @see com.amazonaws.services.dynamodbv2.model.ExpectedAttributeValue + */ + public void deleteIfExists(T object) throws ConditionalCheckFailedException { + DynamoDBDeleteExpression deleteExpression = new DynamoDBDeleteExpression(); + for (DynamoDBMapperFieldModel key : model.keys()) { + deleteExpression.withExpectedEntry(key.name(), ExpectedAttributeValue.builder() + .exists(true).value(key.convert(key.get(object))).build()); + } + mapper.delete(object, deleteExpression); + } + + /** + * Evaluates the specified query expression and returns the count of matching + * items, without returning any of the actual item data + * @param queryExpression The query expression. + * @return The count. + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper#count + */ + public int count(DynamoDBQueryExpression queryExpression) { + return mapper.count(model.targetType(), queryExpression); + } + + /** + * Queries an Amazon DynamoDB table and returns the matching results as an + * unmodifiable list of instantiated objects. + * @param queryExpression The query expression. + * @return The query results. + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper#query + */ + public PaginatedQueryList query(DynamoDBQueryExpression queryExpression) { + return mapper.query(model.targetType(), queryExpression); + } + + /** + * Queries an Amazon DynamoDB table and returns a single page of matching + * results. + * @param queryExpression The query expression. + * @return The query results. + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper#query + */ + public QueryResultPage queryPage(DynamoDBQueryExpression queryExpression) { + return mapper.queryPage(model.targetType(), queryExpression); + } + + /** + * Evaluates the specified scan expression and returns the count of matching + * items, without returning any of the actual item data. + * @param scanExpression The scan expression. + * @return The count. + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper#count + */ + public int count(DynamoDBScanExpression scanExpression) { + return mapper.count(model.targetType(), scanExpression); + } + + /** + * Scans through an Amazon DynamoDB table and returns the matching results + * as an unmodifiable list of instantiated objects. + * @param scanExpression The scan expression. + * @return The scan results. + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper#scan + */ + public PaginatedScanList scan(DynamoDBScanExpression scanExpression) { + return mapper.scan(model.targetType(), scanExpression); + } + + /** + * Scans through an Amazon DynamoDB table and returns a single page of + * matching results. + * @param scanExpression The scan expression. + * @return The scan results. + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper#scanPage + */ + public ScanResultPage scanPage(DynamoDBScanExpression scanExpression) { + return mapper.scanPage(model.targetType(), scanExpression); + } + + /** + * Scans through an Amazon DynamoDB table on logically partitioned segments + * in parallel and returns the matching results in one unmodifiable list of + * instantiated objects. + * @param scanExpression The scan expression. + * @param totalSegments The total segments. + * @return The scan results. + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper#parallelScan + */ + public PaginatedParallelScanList parallelScan(DynamoDBScanExpression scanExpression, int totalSegments) { + return mapper.parallelScan(model.targetType(), scanExpression, totalSegments); + } + + /** + * Returns information about the table, including the current status of the + * table, when it was created, the primary key schema, and any indexes on + * the table. + * @return The describe table results. + * @see com.amazonaws.services.dynamodbv2.AmazonDynamoDB#describeTable + */ + public TableDescription describeTable() { + throw new UnsupportedOperationException("table admin not yet ported to v2"); + } + + /** + * Creates the table with the specified throughput; also populates the same + * throughput for all global secondary indexes. + * @param throughput The provisioned throughput. + * @return The table decription. + * @see com.amazonaws.services.dynamodbv2.AmazonDynamoDB#createTable + * @see com.amazonaws.services.dynamodbv2.model.CreateTableRequest + */ + public TableDescription createTable(ProvisionedThroughput throughput) { + throw new UnsupportedOperationException("table admin not yet ported to v2"); + } + + /** + * Creates the table and ignores the {@code ResourceInUseException} if it + * ialready exists. + * @param throughput The provisioned throughput. + * @return True if created, or false if the table already existed. + * @see com.amazonaws.services.dynamodbv2.AmazonDynamoDB#createTable + * @see com.amazonaws.services.dynamodbv2.model.CreateTableRequest + */ + public boolean createTableIfNotExists(ProvisionedThroughput throughput) { + try { + createTable(throughput); + } catch (final ResourceInUseException e) { + LOG.trace(() -> "Table already exists, no need to create", e); + return false; + } + return true; + } + + /** + * Deletes the table. + * @return The table decription. + * @see com.amazonaws.services.dynamodbv2.AmazonDynamoDB#deleteTable + * @see com.amazonaws.services.dynamodbv2.model.DeleteTableRequest + */ + public TableDescription deleteTable() { + throw new UnsupportedOperationException("table admin not yet ported to v2"); + } + + /** + * Deletes the table and ignores the {@code ResourceNotFoundException} if + * it does not already exist. + * @return True if the table was deleted, or false if the table did not exist. + * @see com.amazonaws.services.dynamodbv2.AmazonDynamoDB#deleteTable + * @see com.amazonaws.services.dynamodbv2.model.DeleteTableRequest + */ + public boolean deleteTableIfExists() { + try { + deleteTable(); + } catch (final ResourceNotFoundException e) { + LOG.trace(() -> "Table does not exist, no need to delete", e); + return false; + } + return true; + } + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBTransactionLoadExpression.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBTransactionLoadExpression.java new file mode 100644 index 000000000000..e457ab5ce349 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBTransactionLoadExpression.java @@ -0,0 +1,75 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import java.util.Map; + +/** + * Enables adding projection expression parameters to transactionLoad operation via TransactionGetRequest. + * For example, you may want to get only specific attributes of an item. + * @see TransactionLoadRequest#addLoad(Object, DynamoDBTransactionLoadExpression) + * @see DynamoDBMapper#transactionLoad(TransactionLoadRequest) + */ +@SdkPublicApi +public final class DynamoDBTransactionLoadExpression { + + /** + * A string that identifies one or more attributes of the specified item to retrieve from the table. The attributes + * in the expression must be separated by commas. If no attribute names are specified, then all attributes of the + * specified item are returned. If any of the requested attributes are not found, they do not appear in the result. + */ + private String projectionExpression; + + /** + * One or more substitution tokens for attribute names in the ProjectionExpression parameter. + */ + private Map expressionAttributeNames; + + /** + * A string that identifies one or more attributes of the specified item to retrieve from the table. The attributes + * in the expression must be separated by commas. If no attribute names are specified, then all attributes of the + * specified item are returned. If any of the requested attributes are not found, they do not appear in the result. + */ + public DynamoDBTransactionLoadExpression withProjectionExpression(String projectionExpression) { + this.projectionExpression = projectionExpression; + return this; + } + + /** + * A string that identifies one or more attributes of the specified item to retrieve from the table. The attributes + * in the expression must be separated by commas. If no attribute names are specified, then all attributes of the + * specified item are returned. If any of the requested attributes are not found, they do not appear in the result. + */ + public String getProjectionExpression() { + return projectionExpression; + } + + /** + * One or more substitution tokens for attribute names in the ProjectionExpression parameter. + */ + public DynamoDBTransactionLoadExpression withExpressionAttributeNames(Map expressionAttributeNames) { + this.expressionAttributeNames = expressionAttributeNames; + return this; + } + + /** + * One or more substitution tokens for attribute names in the ProjectionExpression parameter. + */ + public Map getExpressionAttributeNames() { + return expressionAttributeNames; + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBTransactionWriteExpression.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBTransactionWriteExpression.java new file mode 100644 index 000000000000..56ca1ef381c8 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBTransactionWriteExpression.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.mapper.dynamodb; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +import java.util.Map; + +/** + * Enables adding condition expression parameter to transactionWrite operation via TransactionWriteRequest. + * For example, you may want to update only if an attribute has a particular value. + * @see TransactionWriteRequest#addPut(Object, DynamoDBTransactionWriteExpression) + * @see DynamoDBMapper#transactionWrite(TransactionWriteRequest) + */ +@SdkPublicApi +public final class DynamoDBTransactionWriteExpression { + /** + * A condition that must be satisfied in order for a conditional write to succeed. + */ + private String conditionExpression; + + /** + * One or more substitution tokens for attribute names in an expression. + */ + private Map expressionAttributeNames; + + /** + * One or more values that can be substituted in an expression. + */ + private Map expressionAttributeValues; + + /** + * A condition that must be satisfied in order for a conditional write to succeed. + */ + public DynamoDBTransactionWriteExpression withConditionExpression(String conditionExpression) { + this.conditionExpression = conditionExpression; + return this; + } + + /** + * A condition that must be satisfied in order for a conditional write to succeed. + */ + public String getConditionExpression() { + return conditionExpression; + } + + /** + * One or more substitution tokens for attribute names in an expression. + */ + public DynamoDBTransactionWriteExpression withExpressionAttributeNames(Map expressionAttributeNames) { + this.expressionAttributeNames = expressionAttributeNames; + return this; + } + + /** + * One or more substitution tokens for attribute names in an expression. + */ + public Map getExpressionAttributeNames() { + return expressionAttributeNames; + } + + /** + * One or more values that can be substituted in an expression. + */ + public DynamoDBTransactionWriteExpression withExpressionAttributeValues(Map expressionAttributeValues) { + this.expressionAttributeValues = expressionAttributeValues; + return this; + } + + /** + * One or more values that can be substituted in an expression. + */ + public Map getExpressionAttributeValues() { + return expressionAttributeValues; + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBTypeConverted.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBTypeConverted.java new file mode 100644 index 000000000000..347a259da2a0 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBTypeConverted.java @@ -0,0 +1,113 @@ +/* + * 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.mapper.dynamodb; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to mark a property as using a custom type-converter. + * + *

May be annotated on a user-defined annotation to pass additional + * properties to the {@link DynamoDBTypeConverter}.

+ * + *
+ * @CurrencyFormat(separator=" ") //<- user-defined annotation
+ * public Currency getCurrency()
+ * 
+ * + *

Where,

+ *
+ * public class Currency {
+ *     private Double amount;
+ *     private String unit;
+ *
+ *     public Double getAmount() { return amount; }
+ *     public void setAmount(Double amount) { this.amount = amount; }
+ *
+ *     public String getUnit() { return unit; }
+ *     public void setUnit(String unit) { this.unit = unit; }
+ * }
+ * 
+ * + *

And user-defined annotation,

+ *
+ * @Target({ElementType.METHOD})
+ * @Retention(RetentionPolicy.RUNTIME)
+ * @DynamoDBTypeConverted(converter=CurrencyFormat.Converter.class)
+ * public @interface CurrencyFormat {
+ *
+ *     String separator() default " ";
+ *
+ *     public static class Converter implements DynamoDBTypeConverter<String,Currency> {
+ *         private final String separator;
+ *         public Converter(final Class<Currency> targetType, final CurrencyFormat annotation) {
+ *             this.separator = annotation.separator();
+ *         }
+ *         public Converter() {
+ *             this.separator = "|";
+ *         }
+ *         @Override
+ *         public String convert(final Currency o) {
+ *             return String.valueOf(o.getAmount()) + separator + o.getUnit();
+ *         }
+ *         @Override
+ *         public Currency unconvert(final String o) {
+ *             final String[] strings = o.split(separator);
+ *             final Currency currency = new Currency();
+ *             currency.setAmount(Double.valueOf(strings[0]));
+ *             currency.setUnit(strings[1]);
+ *             return currency;
+ *         }
+ *     }
+ * }
+ * 
+ * + *

Alternately, the property/field may be annotated directly (which + * requires the converter to provide a default constructor or a constructor + * with only the {@code targetType}),

+ *
+ * @DynamoDBTypeConverted(converter=CurrencyFormat.Converter.class)
+ * public Currency getCurrency() { return currency; }
+ * 
+ * + *

All converters are null-safe, a {@code null} value will never be passed + * to {@link DynamoDBTypeConverter#convert} + * or {@link DynamoDBTypeConverter#unconvert}.

+ * + *

Precedence for selecting a type-converter first goes to getter annotations, + * then field, then finally type.

+ * + *

May be used in combination with {@link DynamoDBTyped} to specify the + * attribute type binding.

+ *

Compatible with {@link DynamoDBAutoGeneratedTimestamp}

+ * + *

May be used as a meta-annotation.

+ */ +@DynamoDB +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +public @interface DynamoDBTypeConverted { + + /** + * The class of the converter for this property. + */ + @SuppressWarnings("rawtypes") + Class converter(); + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBTypeConvertedEnum.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBTypeConvertedEnum.java new file mode 100644 index 000000000000..83dac3da960d --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBTypeConvertedEnum.java @@ -0,0 +1,48 @@ +/* + * 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.mapper.dynamodb; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to convert the enumeration value to a string. + * + *

Alternately, the {@link DynamoDBTyped} annotation may be used,

+ *
+ * public static enum Status { OPEN, PENDING, CLOSED }
+ *
+ * @DynamoDBTyped(DynamoDBAttributeType.S)
+ * public Status getStatus()
+ * 
+ * + *

Please note, there are some risks in distributed systems when using + * enumerations as attributes instead of simply using a String. + * When adding new values to the enumeration, the enum only changes must + * be deployed before the enumeration value can be persisted. This will + * ensure that all systems have the correct code to map it from the item + * record in DynamoDB to your objects.

+ * + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBTypeConverted + */ +@DynamoDBTyped(DynamoDBMapperFieldModel.DynamoDBAttributeType.S) +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD}) +public @interface DynamoDBTypeConvertedEnum { + +} \ No newline at end of file diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBTypeConvertedEpochDate.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBTypeConvertedEpochDate.java new file mode 100644 index 000000000000..8be2e8dcf52f --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBTypeConvertedEpochDate.java @@ -0,0 +1,40 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.mapper.dynamodb; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to convert a date object ({@link java.util.Date}, {@link java.util.Calendar}, {@link org.joda.time.DateTime}) + * to a {@link software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType#N} stored as epoch time. + * + *

Alternately, the {@link DynamoDBTyped} annotation may be used,

+ *
+ * @DynamoDBTyped(DynamoDBAttributeType.N)
+ * public Date getDate()
+ * 
+ * + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBTypeConverted + */ +@DynamoDB +@DynamoDBTyped(DynamoDBMapperFieldModel.DynamoDBAttributeType.N) +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.METHOD}) +public @interface DynamoDBTypeConvertedEpochDate { +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBTypeConvertedJson.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBTypeConvertedJson.java new file mode 100644 index 000000000000..78d7205390cf --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBTypeConvertedJson.java @@ -0,0 +1,101 @@ +/* + * 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.mapper.dynamodb; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; + +/** + * A simple JSON converter that uses the Jackson JSON processor. + * + *

It shares all limitations of that library. For more information about + * Jackson, see: http://wiki.fasterxml.com/JacksonHome

+ * + *
+ * @DynamoDBTypeConvertedJson
+ * public Currency getCurrency()
+ * 
+ * + *

Where,

+ *
+ * public class Currency {
+ *     private Double amount;
+ *     private String unit;
+ *
+ *     public Double getAmount() { return amount; }
+ *     public void setAmount(Double amount) { this.amount = amount; }
+ *
+ *     public String getUnit() { return unit; }
+ *     public void setUnit(String unit) { this.unit = unit; }
+ * }
+ * 
+ * + *

Would write the following value to DynamoDB given,

+ *
    + *
  • Currency(79.99,"USD") = "{\"amount\":79.99,\"unit\":\"USD\"}"
  • + *
+ * + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBTypeConverted + */ +@DynamoDBTypeConverted(converter=DynamoDBTypeConvertedJson.Converter.class) +@DynamoDBTyped(DynamoDBMapperFieldModel.DynamoDBAttributeType.S) +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD}) +public @interface DynamoDBTypeConvertedJson { + + /** + * The value type to use when calling the JSON mapper's {@code readValue}; + * a value of {@code Void.class} indicates to use the getter's type. + */ + Class targetType() default void.class; + + /** + * JSON type converter. + */ + final class Converter implements DynamoDBTypeConverter { + private static final ObjectMapper mapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + private final Class targetType; + + public Converter(Class targetType, DynamoDBTypeConvertedJson annotation) { + this.targetType = annotation.targetType() == void.class ? targetType : (Class)annotation.targetType(); + } + + @Override + public final String convert(final T object) { + try { + return mapper.writeValueAsString(object); + } catch (final Exception e) { + throw new DynamoDBMappingException("Unable to write object to JSON", e); + } + } + + @Override + public final T unconvert(final String object) { + try { + return mapper.readValue(object, targetType); + } catch (final Exception e) { + throw new DynamoDBMappingException("Unable to read JSON string", e); + } + } + } + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBTypeConvertedTimestamp.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBTypeConvertedTimestamp.java new file mode 100644 index 000000000000..e8bb217ce0b4 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBTypeConvertedTimestamp.java @@ -0,0 +1,96 @@ +/* + * 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.mapper.dynamodb; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import java.util.Date; +import java.util.TimeZone; + +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; + +/** + * Annotation to format a timestamp object using Java's standard date and time + * patterns. + * + *
+ * @DynamoDBTypeConvertedTimestamp(pattern="yyyyMMddHHmmssSSS", timeZone="UTC")
+ * public Date getCreatedDate()
+ * 
+ * + *

Supports the standard {@link Date} type-conversions; such as + * {@link java.util.Calendar}, {@link Long}.

+ * + *

Primitives such as {@code long} are not supported since the unset + * (or null) state can't be detected.

+ * + *

Compatible with {@link DynamoDBAutoGeneratedTimestamp}

+ * + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBAutoGeneratedTimestamp + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBTypeConverted + * @see java.text.SimpleDateFormat + * @see java.util.TimeZone + */ +@DynamoDBTypeConverted(converter=DynamoDBTypeConvertedTimestamp.Converter.class) +@DynamoDBTyped(DynamoDBMapperFieldModel.DynamoDBAttributeType.S) +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD}) +public @interface DynamoDBTypeConvertedTimestamp { + + /** + * The pattern format; default is ISO8601. + * @see java.text.SimpleDateFormat + */ + String pattern() default "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"; + + /** + * The time zone; default is {@code UTC}. + * @see java.util.TimeZone + */ + String timeZone() default "UTC"; + + /** + * Timestamp format converter. + */ + static final class Converter implements DynamoDBTypeConverter { + private final DynamoDBTypeConverter converter; + private final DateTimeFormatter formatter; + + public Converter(Class targetType, DynamoDBTypeConvertedTimestamp annotation) { + this.formatter = DateTimeFormat.forPattern(annotation.pattern()).withZone( + DateTimeZone.forTimeZone(StandardTypeConverters.Scalar.TIME_ZONE.convert(annotation.timeZone())) + ); + this.converter = StandardTypeConverters.factory().getConverter(DateTime.class, targetType); + } + + @Override + public final String convert(final T object) { + return formatter.print(converter.convert(object)); + } + + @Override + public final T unconvert(final String object) { + return converter.unconvert(formatter.parseDateTime(object)); + } + } + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBTypeConverter.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBTypeConverter.java new file mode 100644 index 000000000000..e16f3065f88b --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBTypeConverter.java @@ -0,0 +1,135 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.mapper.dynamodb; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.annotations.SdkInternalApi; + +/** + * Interface for converting types. + * + * @param The DynamoDB standard type. + * @param The object's field/property type. + */ +@SdkPublicApi +public interface DynamoDBTypeConverter { + + /** + * Turns an object of type T into an object of type S. + */ + S convert(T object); + + /** + * Turns an object of type S into an object of type T. + */ + T unconvert(S object); + + /** + * An abstract converter with additional general purpose functions. + */ + @SdkInternalApi + static abstract class AbstractConverter implements DynamoDBTypeConverter { + public static ExtendedConverter join(DynamoDBTypeConverter source, DynamoDBTypeConverter target) { + return new ExtendedConverter(source, target); + } + + public static NullSafeConverter nullSafe(DynamoDBTypeConverter converter) { + return new NullSafeConverter(converter); + } + + public DynamoDBTypeConverter joinAll(DynamoDBTypeConverter ... targets) { + AbstractConverter converter = (AbstractConverter)nullSafe(); + for (DynamoDBTypeConverter target : targets) { + if (target != null) { + converter = converter.join((DynamoDBTypeConverter)nullSafe(target)); + } + } + return converter; + } + + public ExtendedConverter join(DynamoDBTypeConverter target) { + return AbstractConverter.join(this, target); + } + + public NullSafeConverter nullSafe() { + return AbstractConverter.nullSafe(this); + } + } + + /** + * A converter which wraps a source and target converter. + */ + public static class ExtendedConverter extends AbstractConverter { + private final DynamoDBTypeConverter source; + private final DynamoDBTypeConverter target; + + public ExtendedConverter(DynamoDBTypeConverter source, DynamoDBTypeConverter target) { + this.source = source; + this.target = target; + } + + @Override + public S convert(final T o) { + return source.convert(target.convert(o)); + } + + @Override + public T unconvert(final S o) { + return target.unconvert(source.unconvert(o)); + } + } + + /** + * A general purpose delegating converter. + */ + public static class DelegateConverter extends AbstractConverter { + private final DynamoDBTypeConverter delegate; + + public DelegateConverter(DynamoDBTypeConverter delegate) { + this.delegate = delegate; + } + + @Override + public S convert(final T object) { + return delegate.convert(object); + } + + @Override + public T unconvert(final S object) { + return delegate.unconvert(object); + } + } + + /** + * A converter which evaluates nullability before convert/unconvert. + */ + public static class NullSafeConverter extends DelegateConverter { + public NullSafeConverter(DynamoDBTypeConverter delegate) { + super(delegate); + } + + @Override + public S convert(final T object) { + return object == null ? null : super.convert(object); + } + + @Override + public T unconvert(final S object) { + return object == null ? null : super.unconvert(object); + } + } + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBTypeConverterFactory.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBTypeConverterFactory.java new file mode 100644 index 000000000000..5287e80f4d8f --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBTypeConverterFactory.java @@ -0,0 +1,175 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.mapper.dynamodb.StandardTypeConverters.Vector; + +import java.util.AbstractMap.SimpleImmutableEntry; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; + +/** + * {@link DynamoDBTypeConverter} factory and supporting classes. + * + *

To override standard type-conversions,

+ *
+ * DynamoDBMapperConfig config = DynamoDBMapperConfig.builder()
+ *     .withTypeConverterFactory(DynamoDBTypeConverterFactory.standard().override()
+ *         .with(String.class, MyObject.class, new StringToMyObjectConverter())
+ *         .build())
+ *     .build();
+ * 
+ *

Then, on the property, specify the attribute binding,

+ *
+ * @DynamoDBTyped(DynamoDBAttributeType.S)
+ * public MyObject getMyObject()
+ * 
+ * + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig + */ +@SdkPublicApi +public abstract class DynamoDBTypeConverterFactory { + + /** + * Gets the type-converter matching the target conversion type. + * @param The DynamoDB standard type. + * @param The object's field/property type. + * @param sourceType The source conversion type. + * @param targetType The target conversion type. + * @return The type-converter, or null if no match. + */ + public abstract DynamoDBTypeConverter getConverter(Class sourceType, Class targetType); + + /** + * Creates a type-converter factory builder using this factory as defaults. + */ + public final Builder override() { + return new Builder(this); + } + + /** + * Returns the standard type-converter factory. To override, the factory, + * @see DynamoDBTypeConverterFactory#override + */ + public static final DynamoDBTypeConverterFactory standard() { + return StandardTypeConverters.factory(); + } + + /** + * Builder for overriding type-converters. + */ + public static final class Builder { + private final ConverterMap overrides = new ConverterMap(); + private final DynamoDBTypeConverterFactory defaults; + + private Builder(DynamoDBTypeConverterFactory defaults) { + this.defaults = defaults; + } + + public Builder with(Class sourceType, Class targetType, DynamoDBTypeConverter converter) { + if (Vector.SET.is(sourceType) || Vector.LIST.is(sourceType) || Vector.MAP.is(sourceType)) { + throw new DynamoDBMappingException("type [" + sourceType + "] is not supported" + + "; type-converter factory only supports scalar conversions"); + } + overrides.put(sourceType, targetType, converter); + return this; + } + + public DynamoDBTypeConverterFactory build() { + return new OverrideFactory(defaults, overrides); + } + } + + /** + * A delegating {@link DynamoDBTypeConverterFactory}. + */ + public static class DelegateFactory extends DynamoDBTypeConverterFactory { + private final DynamoDBTypeConverterFactory delegate; + + public DelegateFactory(DynamoDBTypeConverterFactory delegate) { + this.delegate = delegate; + } + + @Override + public DynamoDBTypeConverter getConverter(Class sourceType, Class targetType) { + return delegate.getConverter(sourceType, targetType); + } + } + + /** + * Delegate factory to allow selected types to be overriden. + */ + private static class OverrideFactory extends DelegateFactory { + private final ConverterMap overrides; + + public OverrideFactory(DynamoDBTypeConverterFactory defaults, ConverterMap overrides) { + super(defaults); + this.overrides = overrides; + } + + @Override + public DynamoDBTypeConverter getConverter(Class sourceType, Class targetType) { + DynamoDBTypeConverter converter = overrides.get(sourceType, targetType); + if (converter == null) { + converter = super.getConverter(sourceType, targetType); + } + return converter; + } + } + + /** + * Map of source and target pairs to the converter. + */ + private static final class ConverterMap extends LinkedHashMap,DynamoDBTypeConverter> { + private static final long serialVersionUID = -1L; + + public void put(Class sourceType, Class targetType, DynamoDBTypeConverter converter) { + put(Key.of(sourceType, targetType), converter); + } + + @SuppressWarnings("unchecked") + public DynamoDBTypeConverter get(Class sourceType, Class targetType) { + for (Entry,DynamoDBTypeConverter> entry : entrySet()) { + if (entry.getKey().isAssignableFrom(sourceType, targetType)) { + return (DynamoDBTypeConverter)entry.getValue(); + } + } + return null; + } + } + + /** + * Source and target conversion type pair. + */ + private static final class Key extends SimpleImmutableEntry,Class> { + private static final long serialVersionUID = -1L; + + private Key(Class sourceType, Class targetType) { + super(sourceType, targetType); + } + + public boolean isAssignableFrom(Class sourceType, Class targetType) { + return getKey().isAssignableFrom(sourceType) && getValue().isAssignableFrom(targetType); + } + + public static Key of(Class sourceType, Class targetType) { + return new Key(sourceType, targetType); + } + } + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBTyped.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBTyped.java new file mode 100644 index 000000000000..82a566884f0f --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBTyped.java @@ -0,0 +1,166 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperFieldModel.DynamoDBAttributeType; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to override the standard attribute type binding. + * + *
+ * @DynamoDBTyped(DynamoDBAttributeType.S)
+ * public MyObject getMyObject()
+ * 
+ + *

Standard Types

+ *

Standard types do not require the annotation if applying the default + * attribute binding for that type.

+ *

String/{@code S} types,

+ *
    + *
  • {@link java.lang.Character}/{@code char}
  • + *
  • {@link java.lang.String}
  • + *
  • {@link java.net.URL}
  • + *
  • {@link java.net.URI}
  • + *
  • {@link java.util.Calendar}
  • + *
  • {@link java.util.Currency}
  • + *
  • {@link java.util.Date}
  • + *
  • {@link java.util.Locale}
  • + *
  • {@link java.util.TimeZone}
  • + *
  • {@link java.util.UUID}
  • + *
  • {@link S3Link}
  • + *
+ *

Number/{@code N} types,

+ *
    + *
  • {@link java.math.BigDecimal}
  • + *
  • {@link java.math.BigInteger}
  • + *
  • {@link java.lang.Boolean}/{@code boolean}
  • + *
  • {@link java.lang.Byte}/{@code byte}
  • + *
  • {@link java.lang.Double}/{@code double}
  • + *
  • {@link java.lang.Float}/{@code float}
  • + *
  • {@link java.lang.Integer}/{@code int}
  • + *
  • {@link java.lang.Long}/{@code long}
  • + *
  • {@link java.lang.Short}/{@code short}
  • + *
+ *

Binary/{@code B} types,

+ *
    + *
  • {@link java.nio.ByteBuffer}
  • + *
  • {@code byte[]}
  • + *
+ * + *

{@link DynamoDBTypeConverter}

+ *

A custom type-converter maybe applied to any attribute, either by + * annotation or by overriding the standard type-converter factory.

+ *
+ * DynamoDBMapperConfig config = DynamoDBMapperConfig.builder()
+ *     .withTypeConverterFactory(DynamoDBTypeConverterFactory.standard().override()
+ *         .with(String.class, MyObject.class, new StringToMyObjectConverter())
+ *         .build())
+ *     .build();
+ * 
+ *

If the converter being applied is already a supported data type and + * the conversion is of the same attribute type, for instance, + * {@link java.util.Date} to {@link String} to {@code S}, + * the annotation may be omited. The annotation is require for all non-standard + * types or if the attribute type binding is being overriden.

+ * + *

{@link com.amazonaws.services.dynamodbv2.model.AttributeValue}

+ *

Direct native conversion is supported by default in all schemas. + * If the attribute is a primary or index key, it must specify either + * {@code B}, {@code N}, or {@code S}, otherwise, it may be omited.

+ * + *

{@link Boolean} to {@code BOOL}

+ *

The standard V2 conversion schema will by default serialize booleans + * natively using the DynamoDB {@code BOOL} type.

+ *
+ * @DynamoDBTyped(DynamoDBAttributeType.BOOL)
+ * public boolean isTesting()
+ * 
+ * + *

{@link Boolean} to {@code N}

+ *

The standard V1 and V2 compatible conversion schemas will by default + * serialize booleans using the DynamoDB {@code N} type, with a value of '1' + * representing 'true' and a value of '0' representing 'false'.

+ *
+ * @DynamoDBTyped(DynamoDBAttributeType.N)
+ * public boolean isTesting()
+ * 
+ * + *

{@link Enum} to {@code S}

+ *

The {@code enum} type is only supported by override or custom converter. + * There are some risks in distributed systems when using enumerations as + * attributes instead of simply using a String. When adding new values to the + * enumeration, the enum only changes must deployed before the enumeration + * value can be persisted. This will ensure that all systems have the correct + * code to map it from the item record in DynamoDB to your objects.

+ *
+ * public enum Status { OPEN, PENDING, CLOSED };
+ *
+ * @DynamoDBTyped(DynamoDBAttributeType.S)
+ * public Status getStatus()
+ * 
+ * + *

{@link UUID} to {@code B}

+ *

The {@code UUID} type will serialize to {@link String}/{@code S} by + * default in all conversion schemas. The schemas do support serializing to + * {@link ByteBuffer}/{@code B} by override.

+ *
+ * @DynamoDBTyped(DynamoDBAttributeType.B)
+ * public UUID getKey()
+ * 
+ * + *

{@link Set} to {@code L}

+ *

The standard V1 and V2 compatible conversion schemas do not by default + * support non-scalar {@code Set} types. They are supported in V2. In + * non-supported schemas, the {@link List}/{@code L} override may be applied + * to any {@code Set} type.

+ *
+ * @DynamoDBTyped(DynamoDBAttributeType.L)
+ * public Set<MyObject> getMyObjects()
+ * 
+ * + *

{@link Object} to {@code M}

+ *

Also supported as {@link DynamoDBDocument}.

+ *
+ * @DynamoDBTyped(DynamoDBAttributeType.M)
+ * public MyObject getMyObject()
+ * 
+ * + *

May be combined with {@link DynamoDBTypeConverted}.

+ * + *

May be used as a meta-annotation.

+ * + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBTypeConverted + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBTypeConverterFactory + */ +@DynamoDB +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +public @interface DynamoDBTyped { + + /** + * Use when the type of the attribute as stored in DynamoDB should differ + * from the standard type assigned by DynamoDBMapper. + */ + DynamoDBAttributeType value() default DynamoDBAttributeType.NULL; + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBVersionAttribute.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBVersionAttribute.java new file mode 100644 index 000000000000..e29d40f123ff --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBVersionAttribute.java @@ -0,0 +1,56 @@ +/* + * 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.mapper.dynamodb; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation for marking a property as an optimistic locking version attribute. + * + *

Applied to the getter method or the class field for the class's version + * property. If the annotation is applied directly to the class field, the + * corresponding getter and setter must be declared in the same class. + * + *

Alternately, the meta-annotation {@link DynamoDBVersioned} may be used + * to annotate a custom annotation, or directly to the field/getter.

+ * + *

Only nullable, integral numeric types (e.g. Integer, Long) can be used as + * version properties. On a save() operation, the {@link DynamoDBMapper} will + * attempt to increment the version property and assert that the service's value + * matches the client's. New objects will be assigned a version of 1 when saved.

+ *

Note that for batchWrite, and by extension batchSave and batchDelete, no + * version checks are performed, as required by the + * {@link com.amazonaws.services.dynamodbv2.AmazonDynamoDB#batchWriteItem(BatchWriteItemRequest)} + * API.

+ * + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBVersioned + */ +@DynamoDB +@DynamoDBVersioned +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.METHOD}) +public @interface DynamoDBVersionAttribute { + + /** + * Optional parameter when the name of the attribute as stored in DynamoDB + * should differ from the name used by the getter / setter. + */ + String attributeName() default ""; + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBVersioned.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBVersioned.java new file mode 100644 index 000000000000..e99610fda20c --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBVersioned.java @@ -0,0 +1,158 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.mapper.dynamodb.StandardTypeConverters.Scalar; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.math.BigInteger; +import java.util.Arrays; + +/** + * Annotation for marking a property as an optimistic locking version attribute. + * + *
+ * @DynamoDBVersioned
+ * public Long getRecordVersionNumber()
+ * 
+ * + *

Alternately, the convinience annotation {@link DynamoDBVersionAttribute} + * may be used if combining with an attribute name on a field/getter.

+ * + *

Only nullable, integral numeric types (e.g. Integer, Long) can be used as + * version properties. On a save() operation, the {@link DynamoDBMapper} will + * attempt to increment the version property and assert that the service's value + * matches the client's.

+ * + *

New objects will be assigned a version of 1 when saved.

+ * + *

Note that for batchWrite, and by extension batchSave and batchDelete, + * no version checks are performed, as required by the + * {@link com.amazonaws.services.dynamodbv2.AmazonDynamoDB#batchWriteItem(BatchWriteItemRequest)} + * API.

+ * + *

May be used as a meta-annotation.

+ * + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBVersionAttribute + */ +@DynamoDB +@DynamoDBAutoGenerated(generator=DynamoDBVersioned.Generator.class) +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +public @interface DynamoDBVersioned { + + /** + * Version auto-generator. + */ + static final class Generator extends DynamoDBAutoGenerator.AbstractGenerator { + private final Sequence sequence; + + public Generator(Class targetType, DynamoDBVersioned annotation) { + super(DynamoDBAutoGenerateStrategy.ALWAYS); + this.sequence = Sequences.of(targetType); + } + + @Override + public final T generate(final T currentValue) { + return currentValue == null ? sequence.init() : sequence.next(currentValue); + } + + static interface Sequence { + public T init(); + public T next(final T o); + } + + private static enum Sequences { + BIG_INTEGER(Scalar.BIG_INTEGER, new Sequence() { + @Override + public final BigInteger init() { + return BigInteger.ONE; + } + @Override + public final BigInteger next(final BigInteger o) { + return o.add(BigInteger.ONE); + } + }), + + BYTE(Scalar.BYTE, new Sequence() { + @Override + public final Byte init() { + return Byte.valueOf((byte)1); + } + @Override + public final Byte next(final Byte o) { + return (byte)((o + 1) % Byte.MAX_VALUE); + } + }), + + INTEGER(Scalar.INTEGER, new Sequence() { + @Override + public final Integer init() { + return Integer.valueOf(1); + } + @Override + public final Integer next(final Integer o) { + return o + 1; + } + }), + + LONG(Scalar.LONG, new Sequence() { + @Override + public final Long init() { + return Long.valueOf(1L); + } + @Override + public final Long next(final Long o) { + return o + 1L; + } + }), + + SHORT(Scalar.SHORT, new Sequence() { + @Override + public final Short init() { + return Short.valueOf((short)1); + } + @Override + public final Short next(final Short o) { + return (short)(o + 1); + } + }); + + private final Sequence sequence; + private final Scalar scalar; + + private Sequences(final Scalar scalar, final Sequence sequence) { + this.sequence = sequence; + this.scalar = scalar; + } + + private static final Sequence of(final Class targetType) { + for (Sequences s : Sequences.values()) { + if (s.scalar.is(targetType)) { + return (Sequence)s.sequence; + } + } + throw new DynamoDBMappingException( + "type [" + targetType + "] is not supported; allowed only " + Arrays.toString(Sequences.values()) + ); + } + } + } + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/IDynamoDBMapper.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/IDynamoDBMapper.java new file mode 100644 index 000000000000..a8489709f9f3 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/IDynamoDBMapper.java @@ -0,0 +1,776 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper.FailedBatch; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.PaginationLoadingStrategy; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.SaveBehavior; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import com.amazonaws.services.dynamodbv2.model.CreateTableRequest; +import com.amazonaws.services.dynamodbv2.model.DeleteTableRequest; +import com.amazonaws.services.s3.model.Region; + +import java.util.List; +import java.util.Map; + +/** + * Interface for DynamoDBMapper. + * + *

+ * Note: Do not implement this interface, extend from {@link AbstractDynamoDBMapper} instead. + *

+ * + * @see DynamoDBMapper + * @see AbstractDynamoDBMapper + */ +@SdkPublicApi +public interface IDynamoDBMapper { + /** + * Get the table model for the class, using the default configuration. + * + * @see DynamoDBMapper#getTableModel(Class, DynamoDBMapperConfig) + */ + DynamoDBMapperTableModel getTableModel(Class clazz); + + /** + * Get the table model for the class using the provided configuration override. + */ + DynamoDBMapperTableModel getTableModel(Class clazz, DynamoDBMapperConfig config); + + /** + * Returns an object with the given hash key, or null if no such item exists. + * + * @see DynamoDBMapper#load(Class, Object, Object, DynamoDBMapperConfig) + */ + T load(Class clazz, Object hashKey, DynamoDBMapperConfig config); + + /** + * Returns an object with the given hash key, or null if no such item exists. + * + * @see DynamoDBMapper#load(Class, Object, Object, DynamoDBMapperConfig) + */ + T load(Class clazz, Object hashKey); + + /** + * Returns an object with the given hash and range key, or null if no such item exists. + * + * @see DynamoDBMapper#load(Class, Object, Object, DynamoDBMapperConfig) + */ + T load(Class clazz, Object hashKey, Object rangeKey); + + /** + * Returns an object whose keys match those of the prototype key object given, or null if no + * such item exists. + * + * @param keyObject + * An object of the class to load with the keys values to match. + * @see DynamoDBMapper#load(Object, DynamoDBMapperConfig) + */ + T load(T keyObject); + + /** + * Returns an object whose keys match those of the given prototype key object, or null if no + * such item exists. + * + * @param keyObject + * An object of the class to load with the keys values to match. + * @param config + * Configuration for the service call to retrieve the object from DynamoDB. This + * configuration overrides the default given at construction. + */ + T load(T keyObject, DynamoDBMapperConfig config); + + /** + * Returns an object with the given hash key and range key, or null if no such object exists. + * + * @param clazz + * The class to load, corresponding to a DynamoDB table. + * @param hashKey + * The key of the object. + * @param rangeKey + * The range key of the object, or null for tables without a range key. + * @param config + * Configuration for the service call to retrieve the object from DynamoDB. This + * configuration overrides the default given at construction. + */ + T load(Class clazz, Object hashKey, Object rangeKey, DynamoDBMapperConfig config); + + /** + * Creates and fills in the attributes on an instance of the class given with the attributes + * given. + *

+ * This is accomplished by looking for getter methods annotated with an appropriate annotation, + * then looking for matching attribute names in the item attribute map. + *

+ * This method is no longer called by load/scan/query methods. If you are overriding this + * method, please switch to using an AttributeTransformer + * + * @param clazz + * The class to instantiate and hydrate + * @param itemAttributes + * The set of item attributes, keyed by attribute name. + */ + T marshallIntoObject(Class clazz, Map itemAttributes); + + /** + * Unmarshalls the list of item attributes into objects of type clazz. + *

+ * This method is no longer called by load/scan/query methods. If you are overriding this + * method, please switch to using an AttributeTransformer + * + * @see DynamoDBMapper#marshallIntoObject(Class, Map) + */ + List marshallIntoObjects(Class clazz, List> itemAttributes); + + /** + * Saves the object given into DynamoDB, using the default configuration. + * + * @see DynamoDBMapper#save(Object, DynamoDBSaveExpression, DynamoDBMapperConfig) + */ + void save(T object); + + /** + * Saves the object given into DynamoDB, using the default configuration and the specified + * saveExpression. + * + * @see DynamoDBMapper#save(Object, DynamoDBSaveExpression, DynamoDBMapperConfig) + */ + void save(T object, DynamoDBSaveExpression saveExpression); + + /** + * Saves the object given into DynamoDB, using the specified configuration. + * + * @see DynamoDBMapper#save(Object, DynamoDBSaveExpression, DynamoDBMapperConfig) + */ + void save(T object, DynamoDBMapperConfig config); + + /** + * Saves an item in DynamoDB. The service method used is determined by the + * {@link DynamoDBMapperConfig#getSaveBehavior()} value, to use either + * {@link AmazonDynamoDB#putItem} or + * {@link AmazonDynamoDB#updateItem}: + *

    + *
  • UPDATE (default) : UPDATE will not affect unmodeled attributes on a save operation + * and a null value for the modeled attribute will remove it from that item in DynamoDB. Because + * of the limitation of updateItem request, the implementation of UPDATE will send a putItem + * request when a key-only object is being saved, and it will send another updateItem request if + * the given key(s) already exists in the table.
  • + *
  • UPDATE_SKIP_NULL_ATTRIBUTES : Similar to UPDATE except that it ignores any null + * value attribute(s) and will NOT remove them from that item in DynamoDB. It also guarantees to + * send only one single updateItem request, no matter the object is key-only or not.
  • + *
  • CLOBBER : CLOBBER will clear and replace all attributes, included unmodeled ones, + * (delete and recreate) on save. Versioned field constraints will also be disregarded.
  • + *
+ * Any options specified in the saveExpression parameter will be overlaid on any constraints due + * to versioned attributes. + * + * @param object + * The object to save into DynamoDB + * @param saveExpression + * The options to apply to this save request + * @param config + * The configuration to use, which overrides the default provided at object + * construction. + * @see DynamoDBMapperConfig.SaveBehavior + */ + void save(T object, DynamoDBSaveExpression saveExpression, DynamoDBMapperConfig config); + + /** + * Deletes the given object from its DynamoDB table using the default configuration. + */ + void delete(Object object); + + /** + * Deletes the given object from its DynamoDB table using the specified deleteExpression and + * default configuration. + */ + void delete(Object object, DynamoDBDeleteExpression deleteExpression); + + /** + * Deletes the given object from its DynamoDB table using the specified configuration. + */ + void delete(Object object, DynamoDBMapperConfig config); + + /** + * Deletes the given object from its DynamoDB table using the provided deleteExpression and + * provided configuration. Any options specified in the deleteExpression parameter will be + * overlaid on any constraints due to versioned attributes. + * + * @param deleteExpression + * The options to apply to this delete request + * @param config + * Config override object. If {@link SaveBehavior#CLOBBER} is supplied, version + * fields will not be considered when deleting the object. + */ + void delete(T object, DynamoDBDeleteExpression deleteExpression, DynamoDBMapperConfig config); + + /** + * Transactionally writes objects specified by transactionWriteRequest by calling {@link AmazonDynamoDB#transactWriteItems} API. + * Changes to objects which are put or updated are applied in-memory. Such in-memory updates are NOT thread safe. + *

+ * This method ignores any SaveBehavior set on the mapper. Whether an object is put or updated is solely determined by the + * {@link TransactionWriteRequest} method called by user while constructing request object. Furthermore, put and update work + * as if SaveBehavior is set as CLOBBER. + *

+ *

+ * This method supports versioning annotations, but not in conjunction with condition expressions. It throws + * {@link com.amazonaws.SdkClientException} exception if class of any input object is annotated with + * {@link DynamoDBVersionAttribute} or {@link DynamoDBVersioned} and a condition expression is also present + *

+ *

+ * Any exceptions from underlying API are thrown as is. For more information, please refer + * https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactWriteItems.html + *

+ * + * @param transactionWriteRequest + * Specifies objects to write along with appropriate condition expressions. + * + * @see DynamoDBMapper#transactionWrite(TransactionWriteRequest, DynamoDBMapperConfig) + */ + void transactionWrite(TransactionWriteRequest transactionWriteRequest); + + /** + * Transactionally writes objects specified by transactionWriteRequest by calling {@link AmazonDynamoDB#transactWriteItems} API. + * Changes to objects which are put or updated are applied in-memory. Such in-memory updates are NOT thread safe. + *

+ * This method ignores any SaveBehavior set on the mapper. Whether an object is put or updated is solely determined by the + * {@link TransactionWriteRequest} method called by user while constructing request object. Furthermore, put and update work + * as if SaveBehavior is set as CLOBBER. + *

+ *

+ * This method supports versioning annotations, but not in conjunction with condition expressions. It throws + * {@link com.amazonaws.SdkClientException} exception if class of any input object is annotated with + * {@link DynamoDBVersionAttribute} or {@link DynamoDBVersioned} and a condition expression is also present + *

+ *

+ * Any exceptions from underlying API are thrown as is. For more information, please refer + * https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactWriteItems.html + *

+ * + * @param transactionWriteRequest + * Specifies objects to write along with appropriate condition expressions. + * @param config + * Only {@link DynamoDBMapperConfig#getTableNameOverride()}, {@link DynamoDBMapperConfig#getTableNameResolver()}, + * {@link DynamoDBMapperConfig#getObjectTableNameResolver()} and {@link DynamoDBMapperConfig#getTypeConverterFactory()} + * are considered. + * If {@link DynamoDBMapperConfig.TableNameOverride} is specified then, given table override will be used as table name for all input objects. + * + * @see DynamoDBMapper#transactionWrite(TransactionWriteRequest, DynamoDBMapperConfig) + */ + void transactionWrite(TransactionWriteRequest transactionWriteRequest, DynamoDBMapperConfig config); + + /** + * Transactionally loads objects specified by transactionLoadRequest by calling {@link AmazonDynamoDB#transactGetItems} API. + *

+ * Any exceptions from underlying API are thrown as is. For more information, please refer + * https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactGetItems.html + *

+ * + * @param transactionLoadRequest + * Specifies objects to load along with appropriate projection expressions. + * + * @see DynamoDBMapper#transactionLoad(TransactionLoadRequest, DynamoDBMapperConfig) + */ + List transactionLoad(TransactionLoadRequest transactionLoadRequest); + + /** + * Transactionally loads objects specified by transactionLoadRequest by calling {@link AmazonDynamoDB#transactGetItems} API. + *

+ * Any exceptions from underlying API are thrown as is. For more information, please refer + * https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactGetItems.html + *

+ * + * @param transactionLoadRequest + * Specifies objects to load along with appropriate projection expressions. + * @param config + * Only {@link DynamoDBMapperConfig#getTableNameOverride()}, {@link DynamoDBMapperConfig#getTableNameResolver()}, + * {@link DynamoDBMapperConfig#getObjectTableNameResolver()} and {@link DynamoDBMapperConfig#getTypeConverterFactory()} + * are supported. + * If {@link DynamoDBMapperConfig.TableNameOverride} is specified then, given table override will be used as table name for all input objects. + * + * @see DynamoDBMapper#transactionLoad(TransactionLoadRequest, DynamoDBMapperConfig) + */ + List transactionLoad(TransactionLoadRequest transactionLoadRequest, DynamoDBMapperConfig config); + + /** + * Deletes the objects given using one or more calls to the + * {@link AmazonDynamoDB#batchWriteItem} API. No version checks are + * performed, as required by the API. + * + * @see DynamoDBMapper#batchWrite(Iterable, Iterable) + */ + List batchDelete(Iterable objectsToDelete); + + /** + * Deletes the objects given using one or more calls to the + * {@link AmazonDynamoDB#batchWriteItem} API. No version checks are + * performed, as required by the API. + * + * @see DynamoDBMapper#batchWrite(Iterable, Iterable) + */ + List batchDelete(Object... objectsToDelete); + + /** + * Saves the objects given using one or more calls to the + * {@link AmazonDynamoDB#batchWriteItem} API. No version checks are + * performed, as required by the API. + *

+ * This method ignores any SaveBehavior set on the mapper, and always behaves as if + * SaveBehavior.CLOBBER was specified, as the AmazonDynamoDB.batchWriteItem() request does not + * support updating existing items. + *

+ * This method fails to save the batch if the size of an individual object in the batch exceeds + * 400 KB. For more information on batch restrictions see, http://docs.aws.amazon + * .com/amazondynamodb/latest/APIReference/API_BatchWriteItem.html + *

+ * + * @see DynamoDBMapper#batchWrite(Iterable, Iterable) + */ + List batchSave(Iterable objectsToSave); + + /** + * Saves the objects given using one or more calls to the + * {@link AmazonDynamoDB#batchWriteItem} API. No version checks are + * performed, as required by the API. + *

+ * This method ignores any SaveBehavior set on the mapper, and always behaves as if + * SaveBehavior.CLOBBER was specified, as the AmazonDynamoDB.batchWriteItem() request does not + * support updating existing items. * + *

+ * This method fails to save the batch if the size of an individual object in the batch exceeds + * 400 KB. For more information on batch restrictions see, http://docs.aws.amazon + * .com/amazondynamodb/latest/APIReference/API_BatchWriteItem.html + *

+ * + * @see DynamoDBMapper#batchWrite(Iterable, Iterable) + */ + List batchSave(Object... objectsToSave); + + /** + * Saves and deletes the objects given using one or more calls to the + * {@link AmazonDynamoDB#batchWriteItem} API. No version checks are + * performed, as required by the API. + *

+ * This method ignores any SaveBehavior set on the mapper, and always behaves as if + * SaveBehavior.CLOBBER was specified, as the AmazonDynamoDB.batchWriteItem() request does not + * support updating existing items. + *

+ * This method fails to save the batch if the size of an individual object in the batch exceeds + * 400 KB. For more information on batch restrictions see, http://docs.aws.amazon + * .com/amazondynamodb/latest/APIReference/API_BatchWriteItem.html + *

+ *

+ * If one of the write requests is for a table that is not present, this method does not throw a + * ResourceNotFoundException but returns a FailedBatch which includes this exception and the + * unprocessed items. + *

+ * + * @see DynamoDBMapper#batchWrite(Iterable, Iterable) + */ + List batchWrite(Iterable objectsToWrite, Iterable objectsToDelete); + + /** + * Saves and deletes the objects given using one or more calls to the + * {@link AmazonDynamoDB#batchWriteItem} API. Use mapper config to + * control the retry strategy when UnprocessedItems are returned by the BatchWriteItem API + *

+ * This method fails to save the batch if the size of an individual object in the batch exceeds + * 400 KB. For more information on batch restrictions see, http://docs.aws.amazon + * .com/amazondynamodb/latest/APIReference/API_BatchWriteItem.html + *

+ *

+ * If one of the write requests is for a table that is not present, this method does not throw a + * ResourceNotFoundException but returns a FailedBatch which includes this exception and the + * unprocessed items. + *

+ * + * @param objectsToWrite + * A list of objects to save to DynamoDB. No version checks are performed, as + * required by the {@link AmazonDynamoDB#batchWriteItem} API. + * @param objectsToDelete + * A list of objects to delete from DynamoDB. No version checks are performed, + * as required by the {@link AmazonDynamoDB#batchWriteItem} + * API. + * @param config + * Only {@link DynamoDBMapperConfig#getTableNameOverride()} and + * {@link DynamoDBMapperConfig#getBatchWriteRetryStrategy()} are considered. If + * TableNameOverride is specified, all objects in the two parameter lists will be + * considered to belong to the given table override. In particular, this method + * always acts as if SaveBehavior.CLOBBER was specified regardless of the + * value of the config parameter. + * @return A list of failed batches which includes the unprocessed items and the exceptions + * causing the failure. + * @see DynamoDBMapperConfig#getTableNameOverride() + * @see DynamoDBMapperConfig#getBatchWriteRetryStrategy() + */ + List batchWrite(Iterable objectsToWrite, + Iterable objectsToDelete, + DynamoDBMapperConfig config); + + /** + * Retrieves multiple items from multiple tables using their primary keys. + * + * @see DynamoDBMapper#batchLoad(List, DynamoDBMapperConfig) + * @return A map of the loaded objects. Each key in the map is the name of a DynamoDB table. + * Each value in the map is a list of objects that have been loaded from that table. All + * objects for each table can be cast to the associated user defined type that is + * annotated as mapping that table. + * @throws DynamoDBMapper.BatchGetItemException if all the requested items are not processed + * within the maximum number of retries. + */ + Map> batchLoad(Iterable itemsToGet); + + /** + * Retrieves multiple items from multiple tables using their primary keys. + * + * @param itemsToGet + * Key objects, corresponding to the class to fetch, with their primary key values + * set. + * @param config + * Only {@link DynamoDBMapperConfig#getTableNameOverride()} and + * {@link DynamoDBMapperConfig#getConsistentReads()} are considered. + * @return A map of the loaded objects. Each key in the map is the name of a DynamoDB table. + * Each value in the map is a list of objects that have been loaded from that table. All + * objects for each table can be cast to the associated user defined type that is + * annotated as mapping that table. + * @throws DynamoDBMapper.BatchGetItemException if all the requested items are not processed + * within the maximum number of retries. + */ + Map> batchLoad(Iterable itemsToGet, DynamoDBMapperConfig config); + + /** + * Retrieves the attributes for multiple items from multiple tables using their primary keys. + * {@link AmazonDynamoDB#batchGetItem} API. + * + * @return A map of the loaded objects. Each key in the map is the name of a DynamoDB table. + * Each value in the map is a list of objects that have been loaded from that table. All + * objects for each table can be cast to the associated user defined type that is + * annotated as mapping that table. + * @throws DynamoDBMapper.BatchGetItemException if all the requested items are not processed + * within the maximum number of retries. + * @see #batchLoad(List, DynamoDBMapperConfig) + * @see #batchLoad(Map, DynamoDBMapperConfig) + */ + Map> batchLoad(Map, List> itemsToGet); + + /** + * Retrieves multiple items from multiple tables using their primary keys. Valid only for tables + * with a single hash key, or a single hash and range key. For other schemas, use + * {@link DynamoDBMapper#batchLoad(List, DynamoDBMapperConfig)} + * + * @param itemsToGet + * Map from class to load to list of primary key attributes. + * @param config + * Only {@link DynamoDBMapperConfig#getTableNameOverride()} and + * {@link DynamoDBMapperConfig#getConsistentReads()} are considered. + * @return A map of the loaded objects. Each key in the map is the name of a DynamoDB table. + * Each value in the map is a list of objects that have been loaded from that table. All + * objects for each table can be cast to the associated user defined type that is + * annotated as mapping that table. + * @throws DynamoDBMapper.BatchGetItemException if all the requested items are not processed + * within the maximum number of retries. + */ + Map> batchLoad(Map, List> itemsToGet, DynamoDBMapperConfig config); + + /** + * Scans through an Amazon DynamoDB table and returns the matching results as an unmodifiable + * list of instantiated objects, using the default configuration. + * + * @see DynamoDBMapper#scan(Class, DynamoDBScanExpression, DynamoDBMapperConfig) + */ + PaginatedScanList scan(Class clazz, DynamoDBScanExpression scanExpression); + + /** + * Scans through an Amazon DynamoDB table and returns the matching results as an unmodifiable + * list of instantiated objects. The table to scan is determined by looking at the annotations + * on the specified class, which declares where to store the object data in Amazon DynamoDB, and + * the scan expression parameter allows the caller to filter results and control how the scan is + * executed. + *

+ * Callers should be aware that the returned list is unmodifiable, and any attempts to modify + * the list will result in an UnsupportedOperationException. + *

+ * You can specify the pagination loading strategy for this scan operation. By default, the list + * returned is lazily loaded when possible. + * + * @param + * The type of the objects being returned. + * @param clazz + * The class annotated with DynamoDB annotations describing how to store the object + * data in Amazon DynamoDB. + * @param scanExpression + * Details on how to run the scan, including any filters to apply to limit results. + * @param config + * The configuration to use for this scan, which overrides the default provided at + * object construction. + * @return An unmodifiable list of the objects constructed from the results of the scan + * operation. + * @see PaginatedScanList + * @see PaginationLoadingStrategy + */ + PaginatedScanList scan(Class clazz, DynamoDBScanExpression scanExpression, DynamoDBMapperConfig config); + + /** + * Scans through an Amazon DynamoDB table on logically partitioned segments in parallel and + * returns the matching results in one unmodifiable list of instantiated objects, using the + * default configuration. + * + * @see DynamoDBMapper#parallelScan(Class, DynamoDBScanExpression,int, DynamoDBMapperConfig) + */ + PaginatedParallelScanList parallelScan(Class clazz, + DynamoDBScanExpression scanExpression, + int totalSegments); + + /** + * Scans through an Amazon DynamoDB table on logically partitioned segments in parallel. This + * method will create a thread pool of the specified size, and each thread will issue scan + * requests for its assigned segment, following the returned continuation token, until the end + * of its segment. Callers should be responsible for setting the appropriate number of total + * segments. More scan segments would result in better performance but more consumed capacity of + * the table. The results are returned in one unmodifiable list of instantiated objects. The + * table to scan is determined by looking at the annotations on the specified class, which + * declares where to store the object data in Amazon DynamoDB, and the scan expression parameter + * allows the caller to filter results and control how the scan is executed. + *

+ * Callers should be aware that the returned list is unmodifiable, and any attempts to modify + * the list will result in an UnsupportedOperationException. + *

+ * You can specify the pagination loading strategy for this parallel scan operation. By default, + * the list returned is lazily loaded when possible. + * + * @param + * The type of the objects being returned. + * @param clazz + * The class annotated with DynamoDB annotations describing how to store the object + * data in Amazon DynamoDB. + * @param scanExpression + * Details on how to run the scan, including any filters to apply to limit results. + * @param totalSegments + * Number of total parallel scan segments. Range: 1 - 4096 + * @param config + * The configuration to use for this scan, which overrides the default provided at + * object construction. + * @return An unmodifiable list of the objects constructed from the results of the scan + * operation. + * @see PaginatedParallelScanList + * @see PaginationLoadingStrategy + */ + PaginatedParallelScanList parallelScan(Class clazz, + DynamoDBScanExpression scanExpression, + int totalSegments, + DynamoDBMapperConfig config); + + /** + * Scans through an Amazon DynamoDB table and returns a single page of matching results. The + * table to scan is determined by looking at the annotations on the specified class, which + * declares where to store the object data in AWS DynamoDB, and the scan expression parameter + * allows the caller to filter results and control how the scan is executed. + * + * @param + * The type of the objects being returned. + * @param clazz + * The class annotated with DynamoDB annotations describing how to store the object + * data in Amazon DynamoDB. + * @param scanExpression + * Details on how to run the scan, including any filters to apply to limit results. + * @param config + * The configuration to use for this scan, which overrides the default provided at + * object construction. + */ + ScanResultPage scanPage(Class clazz, DynamoDBScanExpression scanExpression, DynamoDBMapperConfig config); + + /** + * Scans through an Amazon DynamoDB table and returns a single page of matching results. + * + * @see DynamoDBMapper#scanPage(Class, DynamoDBScanExpression, DynamoDBMapperConfig) + */ + ScanResultPage scanPage(Class clazz, DynamoDBScanExpression scanExpression); + + /** + * Queries an Amazon DynamoDB table and returns the matching results as an unmodifiable list of + * instantiated objects, using the default configuration. + * + * @see DynamoDBMapper#query(Class, DynamoDBQueryExpression, DynamoDBMapperConfig) + */ + PaginatedQueryList query(Class clazz, DynamoDBQueryExpression queryExpression); + + /** + * Queries an Amazon DynamoDB table and returns the matching results as an unmodifiable list of + * instantiated objects. The table to query is determined by looking at the annotations on the + * specified class, which declares where to store the object data in Amazon DynamoDB, and the + * query expression parameter allows the caller to filter results and control how the query is + * executed. + *

+ * When the query is on any local/global secondary index, callers should be aware that the + * returned object(s) will only contain item attributes that are projected into the index. All + * the other unprojected attributes will be saved as type default values. + *

+ * Callers should also be aware that the returned list is unmodifiable, and any attempts to + * modify the list will result in an UnsupportedOperationException. + *

+ * You can specify the pagination loading strategy for this query operation. By default, the + * list returned is lazily loaded when possible. + * + * @param + * The type of the objects being returned. + * @param clazz + * The class annotated with DynamoDB annotations describing how to store the object + * data in Amazon DynamoDB. + * @param queryExpression + * Details on how to run the query, including any conditions on the key values + * @param config + * The configuration to use for this query, which overrides the default provided at + * object construction. + * @return An unmodifiable list of the objects constructed from the results of the query + * operation. + * @see PaginatedQueryList + * @see PaginationLoadingStrategy + */ + PaginatedQueryList query(Class clazz, + DynamoDBQueryExpression queryExpression, + DynamoDBMapperConfig config); + + /** + * Queries an Amazon DynamoDB table and returns a single page of matching results. The table to + * query is determined by looking at the annotations on the specified class, which declares + * where to store the object data in Amazon DynamoDB, and the query expression parameter allows + * the caller to filter results and control how the query is executed. + * + * @see DynamoDBMapper#queryPage(Class, DynamoDBQueryExpression, DynamoDBMapperConfig) + */ + QueryResultPage queryPage(Class clazz, DynamoDBQueryExpression queryExpression); + + /** + * Queries an Amazon DynamoDB table and returns a single page of matching results. The table to + * query is determined by looking at the annotations on the specified class, which declares + * where to store the object data in Amazon DynamoDB, and the query expression parameter allows + * the caller to filter results and control how the query is executed. + * + * @param + * The type of the objects being returned. + * @param clazz + * The class annotated with DynamoDB annotations describing how to store the object + * data in AWS DynamoDB. + * @param queryExpression + * Details on how to run the query, including any conditions on the key values + * @param config + * The configuration to use for this query, which overrides the default provided at + * object construction. + */ + QueryResultPage queryPage(Class clazz, + DynamoDBQueryExpression queryExpression, + DynamoDBMapperConfig config); + + /** + * Evaluates the specified scan expression and returns the count of matching items, without + * returning any of the actual item data, using the default configuration. + * + * @see DynamoDBMapper#count(Class, DynamoDBScanExpression, DynamoDBMapperConfig) + */ + int count(Class clazz, DynamoDBScanExpression scanExpression); + + /** + * Evaluates the specified scan expression and returns the count of matching items, without + * returning any of the actual item data. + *

+ * This operation will scan your entire table, and can therefore be very expensive. Use with + * caution. + * + * @param clazz + * The class mapped to a DynamoDB table. + * @param scanExpression + * The parameters for running the scan. + * @param config + * The configuration to use for this scan, which overrides the default provided at + * object construction. + * @return The count of matching items, without returning any of the actual item data. + */ + int count(Class clazz, DynamoDBScanExpression scanExpression, DynamoDBMapperConfig config); + + /** + * Evaluates the specified query expression and returns the count of matching items, without + * returning any of the actual item data, using the default configuration. + * + * @see DynamoDBMapper#count(Class, DynamoDBQueryExpression, DynamoDBMapperConfig) + */ + int count(Class clazz, DynamoDBQueryExpression queryExpression); + + /** + * Evaluates the specified query expression and returns the count of matching items, without + * returning any of the actual item data. + * + * @param clazz + * The class mapped to a DynamoDB table. + * @param queryExpression + * The parameters for running the query. + * @param config + * The mapper configuration to use for the query, which overrides the default + * provided at object construction. + * @return The count of matching items, without returning any of the actual item data. + */ + int count(Class clazz, DynamoDBQueryExpression queryExpression, DynamoDBMapperConfig config); + + /** + * Returns the underlying {@link S3ClientCache} for accessing S3. + */ + S3ClientCache getS3ClientCache(); + + /** + * Creates an S3Link with the specified bucket name and key using the default S3 region. This + * method requires the mapper to have been initialized with the necessary credentials for + * accessing S3. + * + * @throws IllegalStateException + * if the mapper has not been constructed with the necessary S3 AWS credentials. + */ + S3Link createS3Link(String bucketName, String key); + + /** + * Creates an S3Link with the specified region, bucket name and key. This method requires the + * mapper to have been initialized with the necessary credentials for accessing S3. + * + * @throws IllegalStateException + * if the mapper has not been constructed with the necessary S3 AWS credentials. + */ + S3Link createS3Link(Region s3region, String bucketName, String key); + + /** + * Creates an S3Link with the specified region, bucket name and key. This method requires the + * mapper to have been initialized with the necessary credentials for accessing S3. + * + * @throws IllegalStateException + * if the mapper has not been constructed with the necessary S3 AWS credentials. + */ + S3Link createS3Link(String s3region, String bucketName, String key); + + /** + * Parse the given POJO class and return the CreateTableRequest for the DynamoDB table it + * represents. Note that the returned request does not include the required + * ProvisionedThroughput parameters for the primary table and the GSIs, and that all secondary + * indexes are initialized with the default projection type - KEY_ONLY. + */ + CreateTableRequest generateCreateTableRequest(Class clazz); + + + /** + * Parse the given POJO class and return the DeleteTableRequest for the DynamoDB table it + * represents. + */ + DeleteTableRequest generateDeleteTableRequest(Class clazz); + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/ItemConverter.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/ItemConverter.java new file mode 100644 index 000000000000..f272bce50639 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/ItemConverter.java @@ -0,0 +1,81 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import java.lang.reflect.Method; +import java.util.Map; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * The concrete realization of a strategy for converting between Java objects + * and DynamoDB AttributeValues. Typically created by a + * {@link ConversionSchema}. + */ +@SdkPublicApi +public interface ItemConverter { + /** + * Returns the metadata (e.g. name, type) of the DynamoDB attribute that the + * return value of the given getter will be converted to. + * + * @param getter the getter method to inspect + * @return the metadata of the DynamoDB attribute that the result of the + * getter will be converted to + */ + DynamoDBMapperFieldModel getFieldModel(Method getter); + + /** + * Converts a Java object into a DynamoDB AttributeValue. Potentially able + * to handle both scalar and complex types. + * + * @param getter the getter that returned the value to be converted + * @param value the value to be converted + * @return the converted AttributeValue + */ + AttributeValue convert(Method getter, Object value); + + /** + * Converts an appropriately-annotated POJO into a Map of AttributeValues. + * + * @param value the POJO to convert + * @return the resulting map of attribute values + */ + Map convert(Object value); + + /** + * Reverses the {@link #convert(Method, Object)} method, turning a + * DynamoDB AttributeValue back into a Java object suitable for passing + * to the given setter. + * + * @param getter the getter for the value to be unconverted + * @param setter the setter for the value to be unconverted + * @param value the attribute value to be unconverted + * @return the unconverted Java object + */ + Object unconvert(Method getter, Method setter, AttributeValue value); + + /** + * Reverses the {@link #convert(Object)} method, turning a map of attribute + * values back into a POJO of the given class. + * + * @param the compile-time type of the object to create + * @param clazz the runtime type of the object to create + * @param values the the map of attribute values to unconvert + * @return the unconverted POJO + */ + T unconvert(Class clazz, Map values); +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/JsonMarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/JsonMarshaller.java new file mode 100644 index 000000000000..198c7e3f4c53 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/JsonMarshaller.java @@ -0,0 +1,83 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; + +/** + * A simple JSON marshaller that uses the Jackson JSON processor. It shares all limitations of that + * library. For more information about Jackson, see: http://wiki.fasterxml.com/JacksonHome + * + * @deprecated Replaced by {@link DynamoDBTypeConvertedJson} + */ +@SdkPublicApi +@Deprecated +public class JsonMarshaller implements DynamoDBMarshaller { + + private static final ObjectMapper mapper = new ObjectMapper(); + private static final ObjectWriter writer = mapper.writer(); + + /** + * The value type. + */ + private final Class valueType; + + /** + * Constructs the JSON marshaller instance. + * @param valueType The value type (for generic type erasure). + */ + public JsonMarshaller(final Class valueType) { + this.valueType = valueType; + } + + /** + * Constructs the JSON marshaller instance. + */ + public JsonMarshaller() { + this(null); + } + + /** + * Gets the value type. + * @return The value type. + */ + protected final Class getValueType() { + return this.valueType; + } + + @Override + public String marshall(T obj) { + + try { + return writer.writeValueAsString(obj); + } catch (Exception e) { + throw MapperExceptions.failure(e, + "Unable to marshall the instance of " + obj.getClass() + "into a string"); + } + } + + @Override + public T unmarshall(Class clazz, String json) { + try { + return mapper.readValue(json, (getValueType() == null ? clazz : getValueType())); + } catch (Exception e) { + throw MapperExceptions.failure(e, "Unable to unmarshall the string " + json + "into " + clazz); + } + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/KeyPair.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/KeyPair.java new file mode 100644 index 000000000000..f4043c35a5bf --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/KeyPair.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.mapper.dynamodb; + +import software.amazon.awssdk.annotations.SdkPublicApi; + +@SdkPublicApi +public final class KeyPair { + private Object hashKey; + private Object rangeKey; + + public KeyPair withHashKey(Object hashkey) { + this.hashKey = hashkey; + return this; + } + + public KeyPair withRangeKey(Object rangeKey) { + this.rangeKey = rangeKey; + return this; + } + + public void setHashKey(Object hashKey) { + this.hashKey = hashKey; + } + + public void setRangeKey(Object rangeKey) { + this.rangeKey = rangeKey; + } + + public Object getHashKey() { + return this.hashKey; + } + + public Object getRangeKey() { + return this.rangeKey; + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/MapperExceptions.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/MapperExceptions.java new file mode 100644 index 000000000000..350365a8237f --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/MapperExceptions.java @@ -0,0 +1,47 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.core.exception.AbortedException; +import software.amazon.awssdk.core.exception.SdkClientException; + +/** + * Port of v1 {@code com.amazonaws.util.Throwables.failure}, using the v2 exception + * hierarchy. A {@link RuntimeException} is returned unchanged (the message argument is + * discarded, matching v1); an {@link Error} is rethrown; an {@link InterruptedException} + * becomes an {@link AbortedException}; any other checked exception becomes an + * {@link SdkClientException} (the v2 analog of v1's {@code AmazonClientException}). + */ +@SdkInternalApi +final class MapperExceptions { + + private MapperExceptions() { + } + + static RuntimeException failure(Throwable t, String message) { + if (t instanceof RuntimeException) { + return (RuntimeException) t; + } + if (t instanceof Error) { + throw (Error) t; + } + if (t instanceof InterruptedException) { + return AbortedException.create(message, t); + } + return SdkClientException.create(message, t); + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/PaginatedList.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/PaginatedList.java new file mode 100644 index 000000000000..f90873728626 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/PaginatedList.java @@ -0,0 +1,525 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.PaginationLoadingStrategy; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; +import java.util.NoSuchElementException; + +/** + * Unmodifiable list supporting paginated result sets from Amazon DynamoDB. + *

+ * Pages of results are fetched lazily from DynamoDB as they are needed. Some + * methods, such as {@link PaginatedList#size()} and + * {@link PaginatedList#toArray()}, require fetching the entire result set + * eagerly. See the javadoc of individual methods for details on which are lazy. + * + * @param + * The domain object type stored in this list. + */ +@SdkPublicApi +public abstract class PaginatedList implements List { + + private static final String UNMODIFIABLE_MESSAGE = "This is an unmodifiable list"; + + private static final String ITERATION_ONLY_UNSUPPORTED_OPERATION_MESSAGE = " is not supported when using ITERATION_ONLY configuration."; + + /** + * Reference to the DynamoDB mapper for marshalling DynamoDB attributes back + * into objects + */ + protected final DynamoDBMapper mapper; + + /** + * The class annotated with DynamoDB tags declaring how to load/store + * objects into DynamoDB + */ + protected final Class clazz; + + /** The client for working with DynamoDB */ + protected final DynamoDbClient dynamo; + + /** Tracks if all results have been loaded yet or not */ + protected boolean allResultsLoaded = false; + + /** + * All currently loaded results for this list. + * + * In ITERATION_ONLY mode, this list will at most keep one page of the + * loaded results, and all previous results will be cleared from the memory. + */ + protected final List allResults; + + /** Lazily loaded next results waiting to be added into allResults */ + protected final List nextResults = new LinkedList(); + + /** The pagination loading strategy for this paginated list **/ + private final PaginationLoadingStrategy paginationLoadingStrategy; + + /** + * Keeps track on whether an iterator of the list has been retrieved. + * Only updated and checked when the list is in ITERATION_ONLY mode. + */ + private boolean iterationStarted = false; + + /** + * Constructs a PaginatedList instance using the default PaginationLoadingStrategy + */ + public PaginatedList(DynamoDBMapper mapper, Class clazz, DynamoDbClient dynamo) { + this(mapper, clazz, dynamo, null); + } + + /** + * Constructs a PaginatedList instance. + * + * @param mapper + * The mapper for marshalling DynamoDB attributes into objects. + * @param clazz + * The class of the annotated model. + * @param dynamo + * The DynamoDB client for making low-level request calls. + * @param paginationLoadingStrategy + * The strategy used for loading paginated results. Caller has to + * explicitly set this parameter, since the DynamoDBMapperConfig + * set in the mapper is not accessible here. If null value is + * provided, LAZY_LOADING will be set by default. + */ + public PaginatedList(DynamoDBMapper mapper, Class clazz, DynamoDbClient dynamo, PaginationLoadingStrategy paginationLoadingStrategy) { + this.mapper = mapper; + this.clazz = clazz; + this.dynamo = dynamo; + this.paginationLoadingStrategy = paginationLoadingStrategy == null ? + PaginationLoadingStrategy.LAZY_LOADING : paginationLoadingStrategy; + + this.allResults = new ArrayList(); + + // Ideally, we should eagerly load all results here as soon as EAGER_LOADING is configured. + // But the implementation of loadAllResults() relies on a fully initialized sub-class object. + // So we have to do this in each sub-class constructor. + } + + /** + * Eagerly loads all results for this list. + *

+ * Not supported in ITERATION_ONLY mode. + *

+ */ + public synchronized void loadAllResults() { + checkUnsupportedOperationForIterationOnlyMode("loadAllResults()"); + + if ( allResultsLoaded ) + return; + + while ( nextResultsAvailable() ) { + // Keep all loaded results + moveNextResults(false); + } + + allResultsLoaded = true; + } + + /** + * Returns whether there are more results available not yet included in the + * allResults member field. These could already have been fetched and are + * sitting in the nextResults buffer, or they could be fetched from the + * service opportunistically at the time this method is called. A return + * value of true guarantees that nextResults is non-empty. + */ + private boolean nextResultsAvailable() { + return !nextResults.isEmpty() || loadNextResults(); + } + + /** + * Attempts to load the next batch of results, if there are any, into the + * nextResults buffer. Returns whether there were any results to load. A + * return value of true guarantees that nextResults had items added to it. + */ + private synchronized boolean loadNextResults() { + if ( atEndOfResults() ) + return false; + + do { + nextResults.addAll(fetchNextPage()); + } while ( !atEndOfResults() && nextResults.isEmpty() ); + + return !nextResults.isEmpty(); + } + + /** + * Moves the contents of the nextResults buffer into allResults and resets + * the buffer. + * + * @param clearPreviousResults + * Whether it should clear previous results in allResults field. + */ + private void moveNextResults(boolean clearPreviousResults) { + if (clearPreviousResults) { + allResults.clear(); + } + allResults.addAll(nextResults); + nextResults.clear(); + } + + /** + * Fetches the next page of results (which may be empty) and returns any + * items found. + */ + protected abstract List fetchNextPage(); + + /** + * Returns whether we have reached the end of the result set. + */ + protected abstract boolean atEndOfResults(); + + /** + * Returns an iterator over this list that lazily initializes results as + * necessary. + *

+ * If it is configured with ITERATION_ONLY mode, then the iterator + * could be only retrieved once, and any previously loaded results will be + * cleared in the memory during the iteration. + *

+ */ + @Override + public Iterator iterator() { + return new PaginatedListIterator(paginationLoadingStrategy == PaginationLoadingStrategy.ITERATION_ONLY); + } + + private class PaginatedListIterator implements Iterator { + /** + * Whether this iterator is constructed by a PaginatedList in + * ITERATION_ONLY mode. + */ + private final boolean iterationOnly; + + /** + * A hard copy of the allResults list to prevent + * ConcurrentModificationExceptions. + * Only needed when the list is not in ITERATION_ONLY mode. + */ + private final List allResultsCopy; + + private Iterator innerIterator; + + private int pos = 0; + + public PaginatedListIterator(boolean iterationOnly) { + this.iterationOnly = iterationOnly; + + if (iterationOnly) { + synchronized (PaginatedList.this) { + if (iterationStarted) { + throw new UnsupportedOperationException("The list could only be iterated once in ITERATION_ONLY mode."); + } + iterationStarted = true; + } + + allResultsCopy = null; // not needed for ITERATION_ONLY mode + innerIterator = allResults.iterator(); + } + else { + /* + * We make a copy of the allResults list to iterate over in order to + * avoid ConcurrentModificationExceptions caused by other methods + * loading more results into the list while someone iterates over it. + * This is a more severe problem than it might seem, because even + * innocuous-seeming operations such as contains() can modify the + * underlying result set. + */ + allResultsCopy = new ArrayList(); + allResultsCopy.addAll(allResults); + innerIterator = allResultsCopy.iterator(); + } + } + + @Override + public boolean hasNext() { + return innerIterator.hasNext() || shouldSyncWithAllResultsList() || + nextResultsAvailable(); + } + + /** + * If we aren't in ITERATION_ONLY mode then allResults is the authoritative source of + * results. If its size has increased since this iterator was last synched with it then we + * have more results to process and need to re-sync allResultsCopy with allResults. + * + * @return True if more results are available in allResults then what we have currently + * snapshoted in the iterator, false otherwise. + */ + private boolean shouldSyncWithAllResultsList() { + return !iterationOnly && allResults.size() > allResultsCopy.size(); + } + + @Override + public T next() { + if ( !innerIterator.hasNext() ) { + /* + * We need to immediately fetch more results from the service, + * if + * -- it's in ITERATION_ONLY mode (which means innerIterator + * is always pointing at the "real" list of loaded results) + * OR it's not in ITERATION_ONLY and our private copy of the + * result list is already up to date with the full one. + */ + if ( iterationOnly + || allResults.size() == allResultsCopy.size() ) { + if ( !nextResultsAvailable() ) { + throw new NoSuchElementException(); + } + /* Clear previous results if it's in ITERATION_ONLY mode */ + boolean clearPreviousResults = iterationOnly; + moveNextResults(clearPreviousResults); + } + + if ( iterationOnly ) { + /* + * allResults has been replaced with the latest page of results. + */ + innerIterator = allResults.iterator(); + } else { + /* + * Update our private results copy, and then update the inner iterator + */ + if (allResults.size() > allResultsCopy.size()) { + allResultsCopy.addAll(allResults.subList(allResultsCopy.size(), + allResults.size())); + } + + innerIterator = allResultsCopy.listIterator(pos); + } + } + + pos++; + return innerIterator.next(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE); + } + + } + + /** + * Returns whether the collection is empty. At most one (non-empty) page of + * results is loaded to make the check. + *

+ * Not supported in ITERATION_ONLY mode. + *

+ */ + @Override + public boolean isEmpty() { + checkUnsupportedOperationForIterationOnlyMode("isEmpty()"); + + return !iterator().hasNext(); + } + + /** + * Returns the Nth element of the list. Results are loaded until N elements + * are present, if necessary. + *

+ * Not supported in ITERATION_ONLY mode. + *

+ */ + @Override + public T get(int n) { + checkUnsupportedOperationForIterationOnlyMode("get(int n)"); + + while ( allResults.size() <= n && nextResultsAvailable() ) { + moveNextResults(false); + } + + return allResults.get(n); + } + + /** + * Returns whether the collection contains the given element. Results are + * loaded and checked incrementally until a match is found or the end of the + * result set is reached. + *

+ * Not supported in ITERATION_ONLY mode. + *

+ */ + @Override + public boolean contains(Object arg0) { + checkUnsupportedOperationForIterationOnlyMode("contains(Object arg0)"); + + if ( allResults.contains(arg0) ) + return true; + + while ( nextResultsAvailable() ) { + boolean found = nextResults.contains(arg0); + moveNextResults(false); + if ( found ) + return true; + } + + return false; + } + + /** + * Returns a sub-list in the range specified, loading more results as + * necessary. + *

+ * Not supported in ITERATION_ONLY mode. + *

+ */ + @Override + public List subList(int arg0, int arg1) { + checkUnsupportedOperationForIterationOnlyMode("subList(int arg0, int arg1)"); + + while ( allResults.size() < arg1 && nextResultsAvailable() ) { + moveNextResults(false); + } + + return Collections.unmodifiableList(allResults.subList(arg0, arg1)); + } + + /** + * Returns the first index of the object given in the list. Additional + * results are loaded incrementally as necessary. + *

+ * Not supported in ITERATION_ONLY mode. + *

+ */ + @Override + public int indexOf(Object arg0) { + checkUnsupportedOperationForIterationOnlyMode("indexOf(Object org0)"); + + int indexOf = allResults.indexOf(arg0); + if ( indexOf >= 0 ) + return indexOf; + + while ( nextResultsAvailable() ) { + indexOf = nextResults.indexOf(arg0); + int size = allResults.size(); + moveNextResults(false); + if ( indexOf >= 0 ) + return indexOf + size; + } + + return -1; + } + + // Operations requiring the entire result set + + @Override + public int size() { + loadAllResults(); + return allResults.size(); + } + + @Override + public boolean containsAll(Collection arg0) { + loadAllResults(); + return allResults.containsAll(arg0); + } + + @Override + public int lastIndexOf(Object arg0) { + loadAllResults(); + return allResults.lastIndexOf(arg0); + } + + @Override + public Object[] toArray() { + loadAllResults(); + return allResults.toArray(); + } + + @Override + public X[] toArray(X[] a) { + loadAllResults(); + return allResults.toArray(a); + } + + // Unsupported Operations + + @Override + public ListIterator listIterator() { + throw new UnsupportedOperationException("ListIterators are not supported for this list"); + } + + @Override + public ListIterator listIterator(int arg0) { + throw new UnsupportedOperationException("ListIterators are not supported for this list"); + } + + @Override + public boolean remove(Object arg0) { + throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE); + } + + @Override + public T remove(int arg0) { + throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE); + } + + @Override + public boolean removeAll(Collection arg0) { + throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE); + } + + @Override + public boolean retainAll(Collection arg0) { + throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE); + } + + @Override + public T set(int arg0, T arg1) { + throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE); + } + + @Override + public boolean add(T arg0) { + throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE); + } + + @Override + public void add(int arg0, T arg1) { + throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE); + } + + @Override + public boolean addAll(Collection arg0) { + throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE); + } + + @Override + public boolean addAll(int arg0, Collection arg1) { + throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE); + } + + private void checkUnsupportedOperationForIterationOnlyMode(String methodSignature) { + if (this.paginationLoadingStrategy == PaginationLoadingStrategy.ITERATION_ONLY) { + throw new UnsupportedOperationException(methodSignature + ITERATION_ONLY_UNSUPPORTED_OPERATION_MESSAGE); + } + }; +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/PaginatedParallelScanList.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/PaginatedParallelScanList.java new file mode 100644 index 000000000000..1c88888f4832 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/PaginatedParallelScanList.java @@ -0,0 +1,94 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import java.util.LinkedList; +import java.util.List; + +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.PaginationLoadingStrategy; +import software.amazon.awssdk.services.dynamodb.model.ScanResponse; + +/** + * Implementation of the List interface that represents the results from a parallel scan + * in AWS DynamoDB. Paginated results are loaded on demand when the user + * executes an operation that requires them. Some operations, such as size(), + * must fetch the entire list, but results are lazily fetched page by page when + * possible. + *

+ * This is an unmodifiable list, so callers should not invoke any operations + * that modify this list, otherwise they will throw an + * UnsupportedOperationException. + * + * @param + * The type of objects held in this list. + * @see PaginatedList + */ +@SdkPublicApi +public final class PaginatedParallelScanList extends PaginatedList { + + /** The current parallel scan task which contains all the information about the scan request */ + private final ParallelScanTask parallelScanTask; + + private final DynamoDBMapperConfig config; + + public PaginatedParallelScanList( + DynamoDBMapper mapper, + Class clazz, + DynamoDbClient dynamo, + ParallelScanTask parallelScanTask, + PaginationLoadingStrategy paginationLoadingStrategy, + DynamoDBMapperConfig config) { + super(mapper, clazz, dynamo, paginationLoadingStrategy); + + this.parallelScanTask = parallelScanTask; + this.config = config; + + // Marshal the first batch of results in allResults + allResults.addAll(marshalParallelScanResultsIntoObjects(parallelScanTask.getNextBatchOfScanResults())); + + // If the results should be eagerly loaded at once + if (paginationLoadingStrategy == PaginationLoadingStrategy.EAGER_LOADING) { + loadAllResults(); + } + } + + @Override + protected boolean atEndOfResults() { + return parallelScanTask.isAllSegmentScanFinished(); + } + + @Override + protected List fetchNextPage() { + return marshalParallelScanResultsIntoObjects(parallelScanTask.getNextBatchOfScanResults()); + } + + private List marshalParallelScanResultsIntoObjects(List scanResults) { + List allItems = new LinkedList(); + for (ScanResponse scanResult : scanResults) { + if (null != scanResult) { + allItems.addAll(mapper.marshallIntoObjects( + mapper.toParameters( + scanResult.items(), + clazz, + parallelScanTask.getTableName(), + config))); + } + } + return allItems; + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/PaginatedQueryList.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/PaginatedQueryList.java new file mode 100644 index 000000000000..4b9f09ee170e --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/PaginatedQueryList.java @@ -0,0 +1,96 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import java.util.List; + +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.PaginationLoadingStrategy; +import software.amazon.awssdk.services.dynamodb.model.QueryRequest; +import software.amazon.awssdk.services.dynamodb.model.QueryResponse; + +/** + * Implementation of the List interface that represents the results from a query + * in AWS DynamoDB. Paginated results are loaded on demand when the user + * executes an operation that requires them. Some operations, such as size(), + * must fetch the entire list, but results are lazily fetched page by page when + * possible. + *

+ * This is an unmodifiable list, so callers should not invoke any operations + * that modify this list, otherwise they will throw an + * UnsupportedOperationException. + * + * @param + * The type of objects held in this list. + * @see PaginatedList + */ +@SdkPublicApi +public final class PaginatedQueryList extends PaginatedList { + + /** The current query request */ + private QueryRequest queryRequest; + + private final DynamoDBMapperConfig config; + + /** The current results for the last executed query operation */ + private QueryResponse queryResult; + + public PaginatedQueryList( + DynamoDBMapper mapper, + Class clazz, + DynamoDbClient dynamo, + QueryRequest queryRequest, + QueryResponse queryResult, + PaginationLoadingStrategy paginationLoadingStrategy, + DynamoDBMapperConfig config + ) { + super(mapper, clazz, dynamo, paginationLoadingStrategy); + + this.queryRequest = queryRequest; + this.queryResult = queryResult; + this.config = config; + + + allResults.addAll(mapper.marshallIntoObjects( + mapper.toParameters( + queryResult.items(), + clazz, + queryRequest.tableName(), + config))); + + // If the results should be eagerly loaded at once + if (paginationLoadingStrategy == PaginationLoadingStrategy.EAGER_LOADING) { + loadAllResults(); + } + } + + @Override + protected boolean atEndOfResults() { + return !queryResult.hasLastEvaluatedKey() || queryResult.lastEvaluatedKey().isEmpty(); + } + + @Override + protected synchronized List fetchNextPage() { + queryRequest = queryRequest.toBuilder().exclusiveStartKey(queryResult.lastEvaluatedKey()).build(); + queryResult = dynamo.query(queryRequest); + return mapper.marshallIntoObjects(mapper.toParameters( + queryResult.items(), + clazz, + queryRequest.tableName(), + config)); + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/PaginatedScanList.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/PaginatedScanList.java new file mode 100644 index 000000000000..3ff45299935f --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/PaginatedScanList.java @@ -0,0 +1,96 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import java.util.List; + +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.PaginationLoadingStrategy; +import software.amazon.awssdk.services.dynamodb.model.ScanRequest; +import software.amazon.awssdk.services.dynamodb.model.ScanResponse; + +/** + * Implementation of the List interface that represents the results from a scan + * in AWS DynamoDB. Paginated results are loaded on demand when the user + * executes an operation that requires them. Some operations, such as size(), + * must fetch the entire list, but results are lazily fetched page by page when + * possible. + *

+ * This is an unmodifiable list, so callers should not invoke any operations + * that modify this list, otherwise they will throw an + * UnsupportedOperationException. + * + * @param + * The type of objects held in this list. + * @see PaginatedList + */ +@SdkPublicApi +public final class PaginatedScanList extends PaginatedList { + + /** The current scan request */ + private ScanRequest scanRequest; + + private final DynamoDBMapperConfig config; + + /** The current results for the last executed scan operation */ + private ScanResponse scanResult; + + public PaginatedScanList( + DynamoDBMapper mapper, + Class clazz, + DynamoDbClient dynamo, + ScanRequest scanRequest, + ScanResponse scanResult, + PaginationLoadingStrategy paginationLoadingStrategy, + DynamoDBMapperConfig config + ) { + super(mapper, clazz, dynamo, paginationLoadingStrategy); + + this.scanRequest = scanRequest; + this.scanResult = scanResult; + this.config = config; + + allResults.addAll(mapper.marshallIntoObjects( + mapper.toParameters( + scanResult.items(), + clazz, + scanRequest.tableName(), + config))); + + // If the results should be eagerly loaded at once + if (paginationLoadingStrategy == PaginationLoadingStrategy.EAGER_LOADING) { + loadAllResults(); + } + } + + @Override + protected boolean atEndOfResults() { + return !scanResult.hasLastEvaluatedKey() || scanResult.lastEvaluatedKey().isEmpty(); + } + + @Override + protected synchronized List fetchNextPage() { + scanRequest = scanRequest.toBuilder().exclusiveStartKey(scanResult.lastEvaluatedKey()).build(); + scanResult = dynamo.scan(scanRequest); + return mapper.marshallIntoObjects(mapper.toParameters( + scanResult.items(), + clazz, + scanRequest.tableName(), + config)); + } + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/ParallelScanTask.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/ParallelScanTask.java new file mode 100644 index 000000000000..ab96488bcf7f --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/ParallelScanTask.java @@ -0,0 +1,285 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.core.exception.SdkException; +import software.amazon.awssdk.annotations.SdkTestInternalApi; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.ScanRequest; +import software.amazon.awssdk.services.dynamodb.model.ScanResponse; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +@SdkPublicApi +public final class ParallelScanTask { + + /** + * The list of hard copies of ScanRequest with different segment number. + */ + private final List parallelScanRequests; + + private final int totalSegments; + + /** + * Cache all the future tasks, so that we can extract the exception when + * we see failed segment scan. + */ + private final List> segmentScanFutureTasks; + + /** + * Cache all the most recent ScanResponse on each segment. + */ + private final List segmentScanResults; + + /** + * The current state of the scan on each segment. + * Used as the monitor for synchronization. + */ + private final List segmentScanStates; + + private ExecutorService executorService; + + private final DynamoDbClient dynamo; + + @Deprecated + public ParallelScanTask(DynamoDBMapper mapper, DynamoDbClient dynamo, List parallelScanRequests) { + this(dynamo, parallelScanRequests); + } + + ParallelScanTask(DynamoDbClient dynamo, List parallelScanRequests) { + this(dynamo, parallelScanRequests, Executors.newCachedThreadPool()); + } + + @SdkTestInternalApi + ParallelScanTask(DynamoDbClient dynamo, List parallelScanRequests, + ExecutorService executorService) { + this.dynamo = dynamo; + this.parallelScanRequests = parallelScanRequests; + this.totalSegments = parallelScanRequests.size(); + this.executorService = executorService; + + // Create synchronized views of the list to guarantee any changes are visible across all threads. + segmentScanFutureTasks = Collections + .synchronizedList(new ArrayList>(totalSegments)); + segmentScanResults = Collections.synchronizedList(new ArrayList(totalSegments)); + segmentScanStates = Collections + .synchronizedList(new ArrayList(totalSegments)); + + initSegmentScanStates(); + } + + String getTableName() { + return parallelScanRequests.get(0).tableName(); + } + + public boolean isAllSegmentScanFinished() { + synchronized(segmentScanStates) { + for (int segment = 0; segment < totalSegments; segment++) { + if (segmentScanStates.get(segment) != SegmentScanState.SegmentScanCompleted) + return false; + } + // Shut down if all data have been scanned and loaded. + executorService.shutdown(); + return true; + } + } + + public List getNextBatchOfScanResults() throws SdkClientException { + /** + * Kick-off all the parallel scan tasks. + */ + startScanNextPages(); + /** + * Wait till all the tasks have finished. + */ + synchronized(segmentScanStates) { + while (segmentScanStates.contains(SegmentScanState.Waiting) + || segmentScanStates.contains(SegmentScanState.Scanning)) { + try { + segmentScanStates.wait(); + } catch (InterruptedException ie) { + throw SdkClientException.create("Parallel scan interrupted by other thread.", ie); + } + } + /** + * Keep the lock on segmentScanStates until all the cached results are marshaled and returned. + */ + return marshalParallelScanResults(); + } + + } + + private void startScanNextPages() { + for (int segment = 0; segment < totalSegments; segment++) { + int currentSegment = segment; + SegmentScanState currentSegmentState = segmentScanStates.get(currentSegment); + /** + * Assert: Should never see any task in state of "Scanning" when starting a new batch. + */ + if (currentSegmentState == SegmentScanState.Scanning){ + throw SdkClientException.create("Should never see a 'Scanning' state when starting parallel scans."); + } + /** + * Skip any failed or completed segment, and clear the corresponding cached result. + */ + else if (currentSegmentState == SegmentScanState.Failed + || currentSegmentState == SegmentScanState.SegmentScanCompleted) { + segmentScanResults.set(currentSegment, null); + continue; + } + /** + * Otherwise, submit a new future task and save it in segmentScanFutureTasks. + */ + else { + // Update the state to "Scanning" and notify any waiting thread. + synchronized(segmentScanStates) { + segmentScanStates.set(currentSegment, SegmentScanState.Scanning); + segmentScanStates.notifyAll(); + } + Future futureTask = executorService.submit(new Callable() { + @Override + public ScanResponse call() throws Exception { + try { + if (currentSegmentState == SegmentScanState.HasNextPage) { + return scanNextPageOfSegment(currentSegment, true); + } + else if (currentSegmentState == SegmentScanState.Waiting) { + return scanNextPageOfSegment(currentSegment, false); + } + else { + throw SdkClientException.create("Should not start a new future task"); + } + } catch (Exception e) { + synchronized (segmentScanStates) { + segmentScanStates.set(currentSegment, SegmentScanState.Failed); + segmentScanStates.notifyAll(); + executorService.shutdown(); + } + throw e; + } + } + }); + // Cache the future task (for getting the Exceptions in the working thread). + segmentScanFutureTasks.set(currentSegment, futureTask); + } + } + } + + private List marshalParallelScanResults() { + List scanResults = new LinkedList(); + for (int segment = 0; segment < totalSegments; segment++) { + SegmentScanState currentSegmentState = segmentScanStates.get(segment); + /** + * Rethrow the exception from any failed segment scan. + */ + if (currentSegmentState == SegmentScanState.Failed) { + try { + segmentScanFutureTasks.get(segment).get(); + throw SdkClientException.create("No Exception found in the failed scan task."); + } catch (ExecutionException ee) { + if ( ee.getCause() instanceof SdkException) { + throw (SdkException) (ee.getCause()); + } else { + throw SdkClientException.create("Internal error during the scan on segment #" + segment + ".", + ee.getCause()); + } + } catch (Exception e) { + throw SdkClientException.create("Error during the scan on segment #" + segment + ".", e); + } + } + /** + * Get the ScanResponse from cache if the segment scan has finished. + */ + else if (currentSegmentState == SegmentScanState.HasNextPage + || currentSegmentState == SegmentScanState.SegmentScanCompleted) { + ScanResponse scanResult = segmentScanResults.get(segment); + scanResults.add(scanResult); + } + else if (currentSegmentState == SegmentScanState.Waiting + || currentSegmentState == SegmentScanState.Scanning){ + throw SdkClientException.create("Should never see a 'Scanning' or 'Waiting' state when marshalling parallel scan results."); + } + } + return scanResults; + } + + private ScanResponse scanNextPageOfSegment(int currentSegment, boolean checkLastEvaluatedKey) { + ScanRequest segmentScanRequest = parallelScanRequests.get(currentSegment); + if (checkLastEvaluatedKey) { + ScanResponse lastScanResult = segmentScanResults.get(currentSegment); + segmentScanRequest = segmentScanRequest.toBuilder().exclusiveStartKey(lastScanResult.lastEvaluatedKey()).build(); + } else { + segmentScanRequest = segmentScanRequest.toBuilder().exclusiveStartKey(null).build(); + } + ScanResponse scanResult = dynamo.scan(segmentScanRequest); + + /** + * Cache the scan result in segmentScanResults. + * We should never try to get these scan results by calling get() on the cached future tasks. + */ + segmentScanResults.set(currentSegment, scanResult); + + /** + * Update the state and notify any waiting thread. + */ + synchronized(segmentScanStates) { + if (!scanResult.hasLastEvaluatedKey() || scanResult.lastEvaluatedKey().isEmpty()) + segmentScanStates.set(currentSegment, SegmentScanState.SegmentScanCompleted); + else + segmentScanStates.set(currentSegment, SegmentScanState.HasNextPage); + segmentScanStates.notifyAll(); + } + return scanResult; + } + + private void initSegmentScanStates() { + for (int segment = 0; segment < totalSegments; segment++) { + segmentScanFutureTasks.add(null); + segmentScanResults.add(null); + segmentScanStates.add(SegmentScanState.Waiting); + } + } + + /** + * Enumeration of the possible states of the scan on a segment. + */ + private static enum SegmentScanState { + /** The scan on the segment is waiting for resources to execute and has not started yet. */ + Waiting, + + /** The scan is in process, and hasn't finished yet. */ + Scanning, + + /** The scan has already failed. */ + Failed, + + /** The scan on the current page has finished, but there are more pages in the segment to be scanned. */ + HasNextPage, + + /** The scan on the whole segment has completed. */ + SegmentScanCompleted, + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/QueryResultPage.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/QueryResultPage.java new file mode 100644 index 000000000000..6aaba5ae2677 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/QueryResultPage.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.mapper.dynamodb; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import java.util.List; +import java.util.Map; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.ConsumedCapacity; + +/** + * Container for a page of query results + */ +@SdkPublicApi +public final class QueryResultPage { + + private List results; + private Map lastEvaluatedKey; + + private Integer count; + private Integer scannedCount; + private ConsumedCapacity consumedCapacity; + + /** + * Returns all matching items for this page of query results. + */ + public List getResults() { + return results; + } + + public void setResults(List results) { + this.results = results; + } + + /** + * Returns the last evaluated key, which can be used as the + * exclusiveStartKey to fetch the next page of results. Returns null if this + * is the last page of results. + * + * @return The key-value pairs which map from the attribute name of each component + * of the primary key to its value. + */ + public Map getLastEvaluatedKey() { + return lastEvaluatedKey; + } + + public void setLastEvaluatedKey(Map lastEvaluatedKey) { + this.lastEvaluatedKey = lastEvaluatedKey; + } + + /** + * The number of items in the response.

If you used a + * QueryFilter in the request, then Count is the number of + * items returned after the filter was applied, and ScannedCount + * is the number of matching items before> the filter was applied.

If + * you did not use a filter in the request, then Count and + * ScannedCount are the same. + * + * @return The number of items in the response.

If you used a + * QueryFilter in the request, then Count is the number of + * items returned after the filter was applied, and ScannedCount + * is the number of matching items before> the filter was applied.

If + * you did not use a filter in the request, then Count and + * ScannedCount are the same. + */ + public Integer getCount() { + return count; + } + + public void setCount(Integer count) { + this.count = count; + } + + /** + * The number of items evaluated, before any QueryFilter is + * applied. A high ScannedCount value with few, or no, + * Count results indicates an inefficient Query operation. + * For more information, see Count + * and ScannedCount in the Amazon DynamoDB Developer Guide. + *

If you did not use a filter in the request, then + * ScannedCount is the same as Count. + * + * @return The number of items evaluated, before any QueryFilter is + * applied. A high ScannedCount value with few, or no, + * Count results indicates an inefficient Query operation. + * For more information, see Count + * and ScannedCount in the Amazon DynamoDB Developer Guide. + *

If you did not use a filter in the request, then + * ScannedCount is the same as Count. + */ + public Integer getScannedCount() { + return scannedCount; + } + + public void setScannedCount(Integer scannedCount) { + this.scannedCount = scannedCount; + } + + /** + * The capacity units consumed by an operation. The data returned + * includes the total provisioned throughput consumed, along with + * statistics for the table and any indexes involved in the operation. + * ConsumedCapacity is only returned if the request asked for it. + * For more information, see Provisioned + * Throughput in the Amazon DynamoDB Developer Guide. + * + * @return The capacity units consumed by an operation. The data returned + * includes the total provisioned throughput consumed, along with + * statistics for the table and any indexes involved in the operation. + * ConsumedCapacity is only returned if the request asked for it. + * For more information, see Provisioned + * Throughput in the Amazon DynamoDB Developer Guide. + */ + public ConsumedCapacity getConsumedCapacity() { + return consumedCapacity; + } + + public void setConsumedCapacity(ConsumedCapacity consumedCapacity) { + this.consumedCapacity = consumedCapacity; + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/S3ClientCache.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/S3ClientCache.java new file mode 100644 index 000000000000..b3dc82913633 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/S3ClientCache.java @@ -0,0 +1,210 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.auth.AWSCredentialsProvider; +import com.amazonaws.internal.StaticCredentialsProvider; +import com.amazonaws.regions.RegionUtils; +import com.amazonaws.regions.Regions; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.Region; +import com.amazonaws.services.s3.transfer.TransferManager; + +/** + * A smart Map for {@link AmazonS3} objects. {@link S3ClientCache} keeps the + * clients organized by region, and if provided {@link AWSCredentials} will + * create clients on the fly. Otherwise it just return clients given to it with + * {@link #useClient(AmazonS3)}. + */ +@SdkPublicApi +public final class S3ClientCache { + private final ConcurrentMap clientsByRegion = new ConcurrentHashMap(); + private final Map transferManagersByRegion = new ConcurrentHashMap(); + + private final AWSCredentialsProvider awscredentialsProvider; + + @Deprecated + S3ClientCache(AWSCredentials credentials) { + this(new StaticCredentialsProvider(credentials)); + } + + /** + * Create a client cache using the given AWSCredentialsProvider. If + * {@link #getClient(Regions)} or {@link #getTransferManager(Regions)} is + * called and a client has not been provided for the region, the cache will + * instantiate one from the provided {@link AWSCredentialsProvider}. + * + * @param awsCredentialsProvider + * The credentials provider to use when creating new + * {@link AmazonS3}. + */ + S3ClientCache(AWSCredentialsProvider awsCredentialsProvider) { + this.awscredentialsProvider = awsCredentialsProvider; + } + + /** + * Force the client cache to provide a certain client for the region which + * that client is configured. This can be useful to provide clients with + * different {@link com.amazonaws.services.s3.S3ClientOptions} or use a + * {@link com.amazonaws.services.s3.AmazonS3EncryptionClient} in place of a + * regular client. + * + * Using a new client will also forcibly shut down any + * {@link TransferManager} that has been instantiated with that client, with + * the {@link TransferManager#shutdownNow()} method. + * + * @param client + * An {@link AmazonS3} to use in the cache. Its region will + * be detected automatically. + */ + public void useClient(AmazonS3 client) { + String region = client.getRegionName(); + + synchronized (transferManagersByRegion) { + TransferManager tm = transferManagersByRegion.remove(region); + if (tm != null) { + tm.shutdownNow(); + } + clientsByRegion.put(region, client); + } + } + + /** + * Returns a client for the requested region, or throws an exception when + * unable. + * + * @param region + * The region the returned {@link AmazonS3} will be + * configured to use. + * @return A client for the given region from the cache, either instantiated + * automatically from the provided {@link AWSCredentials} or + * provided with {@link #useClient(AmazonS3)}. + * @throws IllegalArgumentException + * When a region is requested that has not been provided to the + * cache with {@link #useClient(AmazonS3)}, and the cache + * has no {@link AWSCredentials} with which a client may be + * instantiated. + */ + public AmazonS3 getClient(Region region) { + if (region == null) { + throw new IllegalArgumentException("S3 region must be specified"); + } + return getClient(region.toAWSRegion().getName()); + } + + /** + * Returns a client for the requested region, or throws an exception when + * unable. + * + * @param region + * The region the returned {@link AmazonS3} will be + * configured to use. + * @return A client for the given region from the cache, either instantiated + * automatically from the provided {@link AWSCredentials} or + * provided with {@link #useClient(AmazonS3)}. + * @throws IllegalArgumentException + * When a region is requested that has not been provided to the + * cache with {@link #useClient(AmazonS3)}, and the cache + * has no {@link AWSCredentials} with which a client may be + * instantiated. + */ + public AmazonS3 getClient(String region) { + if (region == null) { + throw new IllegalArgumentException("S3 region must be specified"); + } + AmazonS3 client = clientsByRegion.get(region); + return client != null ? client : cacheClient(region); + } + + /** + * Returns a new client with region configured to + * region. + * Also updates the clientsByRegion map by associating the + * new client with region. + * + * @param region + * The region the returned {@link AmazonS3} will be + * configured to use. + * @return A new {@link AmazonS3} client with region set to region. + */ + private AmazonS3 cacheClient(String region) { + if (awscredentialsProvider == null) { + throw new IllegalArgumentException("No credentials provider found to connect to S3"); + } + AmazonS3 client = new AmazonS3Client(awscredentialsProvider); + client.setRegion(RegionUtils.getRegion(region)); + clientsByRegion.put(region, client); + return client; + } + + /** + * Returns a {@link TransferManager} for the given region, or throws an + * exception when unable. The returned {@link TransferManager} will always + * be instantiated from whatever {@link AmazonS3} is in the cache, + * whether provided with {@link #useClient(AmazonS3)} or instantiated + * automatically from {@link AWSCredentials}. + * + * Any {@link TransferManager} returned could be shut down if a new + * underlying {@link AmazonS3} is provided with + * {@link #useClient(AmazonS3)}. + * + * @param region + * The region the returned {@link TransferManager} will be + * configured to use. + * @return A transfer manager for the given region from the cache, or one + * instantiated automatically from any existing + * {@link AmazonS3}, + */ + public TransferManager getTransferManager(Region region) { + return getTransferManager(region.toAWSRegion().getName()); + } + + /** + * Returns a {@link TransferManager} for the given region, or throws an + * exception when unable. The returned {@link TransferManager} will always + * be instantiated from whatever {@link AmazonS3} is in the cache, + * whether provided with {@link #useClient(AmazonS3)} or instantiated + * automatically from {@link AWSCredentials}. + * + * Any {@link TransferManager} returned could be shut down if a new + * underlying {@link AmazonS3} is provided with + * {@link #useClient(AmazonS3)}. + * + * @param region + * The region string the returned {@link TransferManager} will be + * configured to use. + * @return A transfer manager for the given region from the cache, or one + * instantiated automatically from any existing + * {@link AmazonS3}, + */ + public TransferManager getTransferManager(String region) { + synchronized (transferManagersByRegion) { + TransferManager tm = transferManagersByRegion.get(region); + if (tm == null) { + tm = new TransferManager(getClient(region)); + transferManagersByRegion.put(region, tm); + } + return tm; + } + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/S3Link.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/S3Link.java new file mode 100644 index 000000000000..037af868fe62 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/S3Link.java @@ -0,0 +1,577 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.net.URL; + +import com.amazonaws.SdkClientException; +import com.amazonaws.auth.AWSCredentialsProvider; +import com.amazonaws.metrics.RequestMetricCollector; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.internal.BucketNameUtils; +import com.amazonaws.services.s3.model.AccessControlList; +import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.GetObjectRequest; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import com.amazonaws.services.s3.model.PutObjectResult; +import com.amazonaws.services.s3.model.Region; +import com.amazonaws.services.s3.model.S3Object; +import com.amazonaws.services.s3.model.S3ObjectInputStream; +import com.amazonaws.services.s3.model.SetObjectAclRequest; +import com.amazonaws.services.s3.transfer.TransferManager; +import com.amazonaws.util.json.Jackson; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * An S3 Link that works with {@link DynamoDBMapper}. + * An S3 link is persisted as a JSON string in DynamoDB. + * This link object can be used directly to upload/download files to S3. + * Alternatively, the underlying + * {@link AmazonS3Client} and {@link TransferManager} can be retrieved to + * provide full access API to S3. + *

+ * For example: + *

+ * AWSCredentialsProvider s3CredentialProvider = ...;
+ * DynamoDBMapper mapper = new DynamoDBMapper(..., s3CredentialProvider);
+ * String username = "jamestkirk";
+ *
+ * User user = new User();
+ * user.setUsername(username);
+ *
+ * // S3 region can be specified, but is optional
+ * S3Link s3link = mapper.createS3Link("my-company-user-avatars", username + ".jpg");
+ * user.setAvatar(s3link);
+ *
+ * // All meta information of the S3 resource is persisted in DynamoDB, including
+ * // region, bucket, and key
+ * mapper.save(user);
+ *
+ * // Upload file to S3 with the link saved in DynamoDB
+ * s3link.uploadFrom(new File("/path/to/all/those/user/avatars/" + username + ".jpg"));
+ * // Download file from S3 via an S3Link
+ * s3link.downloadTo(new File("/path/to/downloads/" + username + ".jpg"));
+ *
+ * // Full S3 API is available via the canonical AmazonS3Client and TransferManager API.
+ * // For example:
+ * AmazonS3Client s3 = s3link.getAmazonS3Client();
+ * TransferManager s3m = s3link.getTransferManager();
+ * // etc.
+ * 
The User pojo class used above:
+ * @DynamoDBTable(tableName = "user-table")
+ * public class User {
+ *     private String username;
+ *     private S3Link avatar;
+ *
+ *     @DynamoDBHashKey
+ *     public String getUsername() {
+ *         return username;
+ *     }
+ *
+ *     public void setUsername(String username) {
+ *         this.username = username;
+ *     }
+ *
+ *     public S3Link getAvatar() {
+ *         return avatar;
+ *     }
+ *
+ *     public void setAvatar(S3Link avatar) {
+ *         this.avatar = avatar;
+ *     }
+ * }
+ * 
+ */ +@SdkPublicApi +public final class S3Link { + private final S3ClientCache s3cc; + private final ID id; + + S3Link(S3ClientCache s3cc, String bucketName, String key) { + this(s3cc, new ID(bucketName, key)); + } + + S3Link(S3ClientCache s3cc, String region, String bucketName, String key) { + this(s3cc, new ID(region, bucketName, key)); + } + + private S3Link(S3ClientCache s3cc, ID id) { + this.s3cc = s3cc; + this.id = id; + + if ( s3cc == null ) { + throw new IllegalArgumentException("S3ClientCache must be configured for use with S3Link"); + } + if ( id == null || id.getBucket() == null || id.getKey() == null ) { + throw new IllegalArgumentException("Bucket and key must be specified for S3Link"); + } + } + + public String getKey() { + return id.getKey(); + } + + public String getBucketName() { + return id.getBucket(); + } + + /** + * Returns the S3 region in {@link Region} format. + *

+ * Do not use this method if {@link S3Link} is created with a region not in {@link Region} enum. + * Use {@link #getRegion()} instead. + *

+ * + * @return S3 region. + */ + public Region getS3Region() { + return Region.fromValue(getRegion()); + } + + /** + * Returns the S3 region as string. + * + * @return region provided when creating the S3Link object. + * If no region is provided during S3Link creation, returns us-east-1. + */ + public String getRegion() { + return id.getRegionId() == null ? "us-east-1" : id.getRegionId(); + } + + /** + * Serializes into a JSON string. + * + * @return The string representation of the link to the S3 resource. + */ + public String toJson() { + return id.toJson(); + } + + /** + * Deserializes from a JSON string. + */ + public static S3Link fromJson(S3ClientCache s3cc, String json) { + ID id = Jackson.fromJsonString(json, ID.class); + return new S3Link(s3cc, id); + } + + public AmazonS3 getAmazonS3Client() { + return s3cc.getClient(getRegion()); + } + + public TransferManager getTransferManager() { + return s3cc.getTransferManager(getRegion()); + } + + /** + * Convenience method to synchronously upload from the given file to the + * Amazon S3 object represented by this S3Link. + * + * @param source + * source file to upload from + * + * @return A {@link PutObjectResult} object containing the information + * returned by Amazon S3 for the newly created object. + */ + public PutObjectResult uploadFrom(final File source) { + return uploadFrom0(source, null); + } + + /** + * Same as {@link #uploadFrom(File)} but allows specifying a + * request metric collector. + */ + public PutObjectResult uploadFrom(final File source, + RequestMetricCollector requestMetricCollector) { + return uploadFrom0(source, requestMetricCollector); + } + + private PutObjectResult uploadFrom0(final File source, + RequestMetricCollector requestMetricCollector) { + PutObjectRequest req = new PutObjectRequest(getBucketName(), getKey(), + source).withRequestMetricCollector(requestMetricCollector); + return getAmazonS3Client().putObject(req); + } + + /** + * Convenience method to synchronously upload from the given buffer to the + * Amazon S3 object represented by this S3Link. + * + * @param buffer + * The buffer containing the data to upload. + * + * @return A {@link PutObjectResult} object containing the information + * returned by Amazon S3 for the newly created object. + */ + public PutObjectResult uploadFrom(final byte[] buffer) { + return uploadFrom0(buffer, null); + } + + /** + * Same as {@link #uploadFrom(byte[])} but allows specifying a + * request metric collector. + */ + public PutObjectResult uploadFrom(final byte[] buffer, + RequestMetricCollector requestMetricCollector) { + return uploadFrom0(buffer, requestMetricCollector); + } + + private PutObjectResult uploadFrom0(final byte[] buffer, + RequestMetricCollector requestMetricCollector) { + ObjectMetadata objectMetadata = new ObjectMetadata(); + objectMetadata.setContentLength(buffer.length); + PutObjectRequest req = new PutObjectRequest(getBucketName(), getKey(), + new ByteArrayInputStream(buffer), objectMetadata) + .withRequestMetricCollector(requestMetricCollector); + return getAmazonS3Client().putObject(req); + } + + /** + * Sets the access control list for the object represented by this S3Link. + * + * Note: Executing this method requires that the object already exists in + * Amazon S3. + * + * @param acl + * The access control list describing the new permissions for the + * object represented by this S3Link. + */ + public void setAcl(CannedAccessControlList acl) { + setAcl0(acl, null); + } + + public void setAcl(CannedAccessControlList acl, RequestMetricCollector col) { + setAcl0(acl, col); + } + + private void setAcl0(CannedAccessControlList acl, RequestMetricCollector col) { + SetObjectAclRequest setObjectAclRequest = new SetObjectAclRequest(getBucketName(), getKey(), acl) + .withRequestMetricCollector(col); + getAmazonS3Client().setObjectAcl(setObjectAclRequest); + } + + /** + * Sets the access control list for the object represented by this S3Link. + * + * Note: Executing this method requires that the object already exists in + * Amazon S3. + * + * @param acl + * The access control list describing the new permissions for the + * object represented by this S3Link. + */ + public void setAcl(AccessControlList acl) { + setAcl0(acl, null); + } + + /** + * Same as {@link #setAcl(AccessControlList)} but allows specifying a + * request metric collector. + */ + public void setAcl(AccessControlList acl, + RequestMetricCollector requestMetricCollector) { + setAcl0(acl, requestMetricCollector); + } + + private void setAcl0(AccessControlList acl, RequestMetricCollector requestMetricCollector) { + SetObjectAclRequest setObjectAclRequest = new SetObjectAclRequest(getBucketName(), getKey(), acl) + .withRequestMetricCollector(requestMetricCollector); + getAmazonS3Client().setObjectAcl(setObjectAclRequest); + } + + /** + * Returns a URL for the location of the object represented by this S3Link. + *

+ * If the object represented by this S3Link has public read permissions (ex: + * {@link CannedAccessControlList#PublicRead}), then this URL can be + * directly accessed to retrieve the object data. + * + * @return A URL for the location of the object represented by this S3Link. + */ + public URL getUrl() { + return getAmazonS3Client().getUrl(getBucketName(), getKey()); + } + + /** + * Convenient method to synchronously download to the specified file from + * the S3 object represented by this S3Link. + * + * @param destination destination file to download to + * + * @return All S3 object metadata for the specified object. + * Returns null if constraints were specified but not met. + */ + public ObjectMetadata downloadTo(final File destination) { + return downloadTo0(destination, null); + } + + /** + * Same as {@link #downloadTo(File)} but allows specifying a + * request metric collector. + */ + public ObjectMetadata downloadTo(final File destination, + RequestMetricCollector requestMetricCollector) { + return downloadTo0(destination, requestMetricCollector); + } + + private ObjectMetadata downloadTo0(final File destination, + RequestMetricCollector requestMetricCollector) { + GetObjectRequest req = new GetObjectRequest(getBucketName(), getKey()) + .withRequestMetricCollector(requestMetricCollector); + return getAmazonS3Client().getObject(req, destination); + } + + /** + * Downloads the data from the object represented by this S3Link to the + * specified output stream. + * + * @param output + * The output stream to write the object's data to. + * + * @return The object's metadata. + */ + public ObjectMetadata downloadTo(final OutputStream output) { + return downloadTo0(output, null); + } + + /** + * Same as {@link #downloadTo(OutputStream)} but allows specifying a + * request metric collector. + */ + public ObjectMetadata downloadTo(final OutputStream output, + RequestMetricCollector requestMetricCollector) { + return downloadTo0(output, requestMetricCollector); + } + + private ObjectMetadata downloadTo0(final OutputStream output, + RequestMetricCollector requestMetricCollector) { + GetObjectRequest req = new GetObjectRequest(getBucketName(), getKey()) + .withRequestMetricCollector(requestMetricCollector); + S3Object s3Object = getAmazonS3Client().getObject(req); + S3ObjectInputStream objectContent = s3Object.getObjectContent(); + + try { + byte[] buffer = new byte[1024 * 10]; + int bytesRead = -1; + while ((bytesRead = objectContent.read(buffer)) > -1) { + output.write(buffer, 0, bytesRead); + } + } catch (IOException ioe) { + objectContent.abort(); + throw new SdkClientException("Unable to transfer content from Amazon S3 to the output stream", ioe); + } finally { + try { objectContent.close(); } catch (IOException ioe) {} + } + + return s3Object.getObjectMetadata(); + } + + /** + * JSON wrapper of an {@link S3Link} identifier, + * which consists of the S3 region id, bucket name and key. + * Sample JSON serialized form: + *

+     * {"s3":{"bucket":"mybucket","key":"mykey","region":"us-west-2"}}
+     * {"s3":{"bucket":"mybucket","key":"mykey","region":null}}
+     * 
+ * Note for S3 a null region means US standard. + *

+ * @see Region#US_Standard + */ + static class ID { + @JsonProperty("s3") + private S3 s3; + + ID() {} // used by Jackson to unmarshall + ID(String bucketName, String key) { + this.s3 = new S3(bucketName, key); + } + ID(String region, String bucketName, String key) { + this.s3 = new S3(region, bucketName, key); + } + ID(S3 s3) { + this.s3 = s3; + } + @JsonProperty("s3") + public S3 getS3() { + return s3; + } + @JsonIgnore + public String getRegionId() { + return s3.getRegionId(); + } + @JsonIgnore + public String getBucket() { + return s3.getBucket(); + } + @JsonIgnore + public String getKey() { + return s3.getKey(); + } + String toJson() { + return Jackson.toJsonString(this); + } + } + + /** + * Internal class for JSON serialization purposes. + *

+ * @see ID + */ + private static class S3 { + + /** + * The name of the S3 bucket containing the object to retrieve. + */ + @JsonProperty("bucket") + private String bucket; + + /** + * The key under which the desired S3 object is stored. + */ + @JsonProperty("key") + private String key; + + /** + * The region id of {@link Region} where the S3 object is stored. + */ + @JsonProperty("region") + private String regionId; + + @SuppressWarnings("unused") + S3() {} // used by Jackson to unmarshall + + /** + * Constructs a new {@link S3} with all the required parameters. + * + * @param bucket + * The name of the bucket containing the desired object. + * @param key + * The key in the specified bucket under which the object is + * stored. + */ + S3(String bucket, String key) { + this(null, bucket, key); + } + + /** + * Constructs a new {@link S3} with all the required parameters. + * + * @param region + * The region where the S3 object is stored. + * @param bucket + * The name of the bucket containing the desired object. + * @param key + * The key in the specified bucket under which the object is + * stored. + */ + S3(String region, String bucket, String key) { + this.regionId = region; + this.bucket = bucket; + this.key = key; + } + + /** + * Gets the name of the bucket containing the object to be downloaded. + * + * @return The name of the bucket containing the object to be downloaded. + */ + @JsonProperty("bucket") + public String getBucket() { + return bucket; + } + + /** + * Gets the key under which the object to be downloaded is stored. + * + * @return The key under which the object to be downloaded is stored. + */ + @JsonProperty("key") + public String getKey() { + return key; + } + + @JsonProperty("region") + public String getRegionId() { + return regionId; + } + } + + /** + * {@link S3Link} factory. + */ + public static final class Factory implements DynamoDBTypeConverter { + static final Factory DEFAULT = new Factory((S3ClientCache)null); + + public static final Factory of(final AWSCredentialsProvider provider) { + return provider == null ? DEFAULT : new Factory(new S3ClientCache(provider)); + } + + private final S3ClientCache s3cc; + + public Factory(final S3ClientCache s3cc) { + this.s3cc = s3cc; + } + + public S3Link createS3Link(Region s3region, String bucketName, String key) { + return createS3Link(convertRegionToString(s3region, bucketName), bucketName, key); + } + + public S3Link createS3Link(String s3region, String bucketName, String key) { + if (getS3ClientCache() == null) { + throw new IllegalStateException("Mapper must be constructed with S3 AWS Credentials to create S3Link"); + } + return new S3Link(getS3ClientCache(), s3region, bucketName, key); + } + + public S3ClientCache getS3ClientCache() { + return this.s3cc; + } + + @Override + public String convert(final S3Link o) { + return o.getBucketName() == null || o.getKey() == null ? null : o.toJson(); + } + + @Override + public S3Link unconvert(final String o) { + return S3Link.fromJson(getS3ClientCache(), o); + } + } + + private static String convertRegionToString(Region region, String bucketName) { + String regionAsString; + if (region == null) { + if (BucketNameUtils.isDNSBucketName(bucketName)) { + regionAsString = Region.US_Standard.getFirstRegionId(); + } else { + throw new IllegalArgumentException("Region must be specified for bucket that cannot be addressed using virtual host style"); + } + } else { + regionAsString = region.getFirstRegionId(); + } + return regionAsString; + } + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/ScanResultPage.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/ScanResultPage.java new file mode 100644 index 000000000000..3d4b1629aec1 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/ScanResultPage.java @@ -0,0 +1,140 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import java.util.List; +import java.util.Map; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.ConsumedCapacity; + +/** + * Container for a page of scan results. + */ +@SdkPublicApi +public final class ScanResultPage { + + private List results; + private Map lastEvaluatedKey; + + private Integer count; + private Integer scannedCount; + private ConsumedCapacity consumedCapacity; + + /** + * Returns all matching items for this page of scan results, which may be + * empty. + */ + public List getResults() { + return results; + } + + public void setResults(List results) { + this.results = results; + } + + /** + * Returns the last evaluated key, which can be used as the + * exclusiveStartKey to fetch the next page of results. Returns null if this + * is the last page of results. + * + * @return The key-value pairs which map from the attribute name of each component + * of the primary key to its value. + */ + public Map getLastEvaluatedKey() { + return lastEvaluatedKey; + } + + public void setLastEvaluatedKey(Map lastEvaluatedKey) { + this.lastEvaluatedKey = lastEvaluatedKey; + } + + /** + * The number of items in the response.

If you set ScanFilter + * in the request, then Count is the number of items returned + * after the filter was applied, and ScannedCount is the number of + * matching items before the filter was applied.

If you did not use a + * filter in the request, then Count is the same as + * ScannedCount. + * + * @return The number of items in the response.

If you set ScanFilter + * in the request, then Count is the number of items returned + * after the filter was applied, and ScannedCount is the number of + * matching items before the filter was applied.

If you did not use a + * filter in the request, then Count is the same as + * ScannedCount. + */ + public Integer getCount() { + return count; + } + + public void setCount(Integer count) { + this.count = count; + } + + /** + * The number of items evaluated, before any ScanFilter is + * applied. A high ScannedCount value with few, or no, + * Count results indicates an inefficient Scan operation. + * For more information, see Count + * and ScannedCount in the Amazon DynamoDB Developer Guide. + *

If you did not use a filter in the request, then + * ScannedCount is the same as Count. + * + * @return The number of items evaluated, before any ScanFilter is + * applied. A high ScannedCount value with few, or no, + * Count results indicates an inefficient Scan operation. + * For more information, see Count + * and ScannedCount in the Amazon DynamoDB Developer Guide. + *

If you did not use a filter in the request, then + * ScannedCount is the same as Count. + */ + public Integer getScannedCount() { + return scannedCount; + } + + public void setScannedCount(Integer scannedCount) { + this.scannedCount = scannedCount; + } + + /** + * The capacity units consumed by an operation. The data returned + * includes the total provisioned throughput consumed, along with + * statistics for the table and any indexes involved in the operation. + * ConsumedCapacity is only returned if the request asked for it. + * For more information, see Provisioned + * Throughput in the Amazon DynamoDB Developer Guide. + * + * @return The capacity units consumed by an operation. The data returned + * includes the total provisioned throughput consumed, along with + * statistics for the table and any indexes involved in the operation. + * ConsumedCapacity is only returned if the request asked for it. + * For more information, see Provisioned + * Throughput in the Amazon DynamoDB Developer Guide. + */ + public ConsumedCapacity getConsumedCapacity() { + return consumedCapacity; + } + + public void setConsumedCapacity(ConsumedCapacity consumedCapacity) { + this.consumedCapacity = consumedCapacity; + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/StandardAnnotationMaps.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/StandardAnnotationMaps.java new file mode 100644 index 000000000000..f3b1412bf344 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/StandardAnnotationMaps.java @@ -0,0 +1,451 @@ +/* + * 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.mapper.dynamodb; + +import static software.amazon.awssdk.mapper.dynamodb.DynamoDBAutoGenerateStrategy.CREATE; +import static software.amazon.awssdk.services.dynamodb.model.KeyType.HASH; +import static software.amazon.awssdk.services.dynamodb.model.KeyType.RANGE; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperFieldModel.DynamoDBAttributeType; +import software.amazon.awssdk.services.dynamodb.model.KeyType; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Map of DynamoDB annotations. + */ +@SdkInternalApi +final class StandardAnnotationMaps { + + private StandardAnnotationMaps() { + } + + /** + * Gets all the DynamoDB annotations for a given class. + */ + static final TableMap of(Class clazz) { + TableMap annotations = new TableMap(clazz); + annotations.putAll(clazz); + return annotations; + } + + /** + * Gets all the DynamoDB annotations; method annotations override field + * level annotations which override class/type level annotations. + */ + static final FieldMap of(Method getter, String defaultName) { + Class targetType = (Class)getter.getReturnType(); + String fieldName = StandardBeanProperties.fieldNameOf(getter); + + Field declaredField = null; + try { + declaredField = getter.getDeclaringClass().getDeclaredField(fieldName); + } catch (final SecurityException e) { + throw new DynamoDBMappingException("no access to field for " + getter, e); + } catch (final NoSuchFieldException no) {} + + if (defaultName == null) { + defaultName = fieldName; + } + + FieldMap annotations = new FieldMap(targetType, defaultName); + annotations.putAll(targetType); + annotations.putAll(declaredField); + annotations.putAll(getter); + return annotations; + } + + /** + * Common type-conversions properties. + */ + private static abstract class AbstractAnnotationMap { + private final Annotations map = new Annotations(); + + /** + * Gets the actual annotation by type; if the type is not directly + * mapped then the meta-annotation is returned. + */ + final A actualOf(final Class annotationType) { + Annotation annotation = this.map.get(annotationType); + if (annotation == null || annotation.annotationType() == annotationType) { + return (A)annotation; + } else if (annotation.annotationType().isAnnotationPresent(annotationType)) { + return annotation.annotationType().getAnnotation(annotationType); + } + throw new DynamoDBMappingException( + "could not resolve annotation by type" + + "; @" + annotationType.getSimpleName() + " not present on " + annotation + ); + } + + /** + * Puts all DynamoDB annotations into the map. + */ + final void putAll(AnnotatedElement annotated) { + if (annotated != null) { + this.map.putAll(new Annotations().putAll(annotated.getAnnotations())); + } + } + } + + /** + * Common type-conversions properties. + */ + static abstract class TypedMap extends AbstractAnnotationMap { + private final Class targetType; + + private TypedMap(final Class targetType) { + this.targetType = targetType; + } + + /** + * Gets the target type. + */ + final Class targetType() { + return this.targetType; + } + + /** + * Gets the attribute type from the {@link DynamoDBTyped} annotation + * if present. + */ + public DynamoDBAttributeType attributeType() { + DynamoDBTyped annotation = actualOf(DynamoDBTyped.class); + if (annotation != null) { + return annotation.value(); + } + return null; + } + + /** + * Creates a new type-converter form the {@link DynamoDBTypeConverted} + * annotation if present. + */ + public DynamoDBTypeConverter typeConverter() { + Annotation annotation = super.map.get(DynamoDBTypeConverted.class); + if (annotation != null) { + DynamoDBTypeConverted converted = actualOf(DynamoDBTypeConverted.class); + annotation = (converted == annotation ? null : annotation); + return overrideOf(converted.converter(), targetType, annotation); + } + return null; + } + + /** + * Creates a new auto-generator from the {@link DynamoDBAutoGenerated} + * annotation if present. + */ + public DynamoDBAutoGenerator autoGenerator() { + Annotation annotation = super.map.get(DynamoDBAutoGenerated.class); + if (annotation != null) { + DynamoDBAutoGenerated generated = actualOf(DynamoDBAutoGenerated.class); + annotation = (generated == annotation ? null : annotation); + DynamoDBAutoGenerator generator = overrideOf(generated.generator(), targetType, annotation); + if (generator.getGenerateStrategy() == CREATE && targetType.isPrimitive()) { + throw new DynamoDBMappingException( + "type [" + targetType + "] is not supported for auto-generation" + + "; primitives are not allowed when auto-generate strategy is CREATE" + ); + } + return generator; + } + return null; + } + + /** + * Maps the attributes from the {@link DynamoDBFlattened} annotation. + */ + public Map attributes() { + Map attributes = new LinkedHashMap(); + for (DynamoDBAttribute a : actualOf(DynamoDBFlattened.class).attributes()) { + if (a.mappedBy().isEmpty() || a.attributeName().isEmpty()) { + throw new DynamoDBMappingException("@DynamoDBFlattened must specify mappedBy and attributeName"); + } else if (attributes.put(a.mappedBy(), a.attributeName()) != null) { + throw new DynamoDBMappingException("@DynamoDBFlattened must not duplicate mappedBy=" + a.mappedBy()); + } + } + if (attributes.isEmpty()) { + throw new DynamoDBMappingException("@DynamoDBFlattened must specify one or more attributes"); + } + return attributes; + } + + /** + * Returns true if the {@link DynamoDBFlattened} annotation is present. + */ + public boolean flattened() { + return actualOf(DynamoDBFlattened.class) != null; + } + } + + /** + * {@link DynamoDBMapperTableModel} annotations. + */ + static final class TableMap extends TypedMap implements DynamoDBMapperTableModel.Properties { + private TableMap(final Class targetType) { + super(targetType); + } + + /** + * {@inheritDoc} + */ + @Override + public DynamoDBAttributeType attributeType() { + DynamoDBAttributeType attributeType = super.attributeType(); + if (attributeType == null && actualOf(DynamoDBTable.class) != null) { + attributeType = DynamoDBAttributeType.M; + } + return attributeType; + } + + /** + * {@inheritDoc} + */ + @Override + public String tableName() { + DynamoDBTable annotation = actualOf(DynamoDBTable.class); + if (annotation != null && !annotation.tableName().isEmpty()) { + return annotation.tableName(); + } + return null; + } + } + + /** + * {@link DynamoDBMapperFieldModel} annotations. + */ + static final class FieldMap extends TypedMap implements DynamoDBMapperFieldModel.Properties { + private final String defaultName; + + private FieldMap(Class targetType, String defaultName) { + super(targetType); + this.defaultName = defaultName; + } + + /** + * Returns true if the {@link DynamoDBIgnore} annotation is present. + */ + public boolean ignored() { + return actualOf(DynamoDBIgnore.class) != null; + } + + /** + * {@inheritDoc} + */ + @Override + public DynamoDBAttributeType attributeType() { + DynamoDBScalarAttribute annotation = actualOf(DynamoDBScalarAttribute.class); + if (annotation != null) { + if (Set.class.isAssignableFrom(targetType())) { + return DynamoDBAttributeType.valueOf(annotation.type().name() + "S"); + } else { + return DynamoDBAttributeType.valueOf(annotation.type().name()); + } + } + return super.attributeType(); + } + + /** + * {@inheritDoc} + */ + @Override + public String attributeName() { + DynamoDBHashKey hashKey = actualOf(DynamoDBHashKey.class); + if (hashKey != null && !hashKey.attributeName().isEmpty()) { + return hashKey.attributeName(); + } + DynamoDBIndexHashKey indexHashKey = actualOf(DynamoDBIndexHashKey.class); + if (indexHashKey != null && !indexHashKey.attributeName().isEmpty()) { + return indexHashKey.attributeName(); + } + DynamoDBRangeKey rangeKey = actualOf(DynamoDBRangeKey.class); + if (rangeKey != null && !rangeKey.attributeName().isEmpty()) { + return rangeKey.attributeName(); + } + DynamoDBIndexRangeKey indexRangeKey = actualOf(DynamoDBIndexRangeKey.class); + if (indexRangeKey != null && !indexRangeKey.attributeName().isEmpty()) { + return indexRangeKey.attributeName(); + } + DynamoDBAttribute attribute = actualOf(DynamoDBAttribute.class); + if (attribute != null && !attribute.attributeName().isEmpty()) { + return attribute.attributeName(); + } + DynamoDBVersionAttribute versionAttribute = actualOf(DynamoDBVersionAttribute.class); + if (versionAttribute != null && !versionAttribute.attributeName().isEmpty()) { + return versionAttribute.attributeName(); + } + DynamoDBScalarAttribute scalarAttribute = actualOf(DynamoDBScalarAttribute.class); + if (scalarAttribute != null && !scalarAttribute.attributeName().isEmpty()) { + return scalarAttribute.attributeName(); + } + DynamoDBNamed annotation = actualOf(DynamoDBNamed.class); + if (annotation != null && !annotation.value().isEmpty()) { + return annotation.value(); + } + return this.defaultName; + } + + /** + * {@inheritDoc} + */ + @Override + public KeyType keyType() { + DynamoDBKeyed annotation = actualOf(DynamoDBKeyed.class); + if (annotation != null) { + return annotation.value(); + } + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean versioned() { + return actualOf(DynamoDBVersioned.class) != null; + } + + /** + * {@inheritDoc} + */ + @Override + public Map> globalSecondaryIndexNames() { + Map> gsis = new EnumMap>(KeyType.class); + DynamoDBIndexHashKey indexHashKey = actualOf(DynamoDBIndexHashKey.class); + if (indexHashKey != null) { + if (!indexHashKey.globalSecondaryIndexName().isEmpty()) { + if (indexHashKey.globalSecondaryIndexNames().length > 0) { + throw new DynamoDBMappingException("@DynamoDBIndexHashKey must not specify both HASH GSI name/names"); + } + gsis.put(HASH, Collections.singletonList(indexHashKey.globalSecondaryIndexName())); + } else if (indexHashKey.globalSecondaryIndexNames().length > 0) { + gsis.put(HASH, Collections.unmodifiableList(Arrays.asList(indexHashKey.globalSecondaryIndexNames()))); + } else { + throw new DynamoDBMappingException("@DynamoDBIndexHashKey must specify one of HASH GSI name/names"); + } + } + DynamoDBIndexRangeKey indexRangeKey = actualOf(DynamoDBIndexRangeKey.class); + if (indexRangeKey != null) { + if (!indexRangeKey.globalSecondaryIndexName().isEmpty()) { + if (indexRangeKey.globalSecondaryIndexNames().length > 0) { + throw new DynamoDBMappingException("@DynamoDBIndexRangeKey must not specify both RANGE GSI name/names"); + } + gsis.put(RANGE, Collections.singletonList(indexRangeKey.globalSecondaryIndexName())); + } else if (indexRangeKey.globalSecondaryIndexNames().length > 0) { + gsis.put(RANGE, Collections.unmodifiableList(Arrays.asList(indexRangeKey.globalSecondaryIndexNames()))); + } else if (localSecondaryIndexNames().isEmpty()) { + throw new DynamoDBMappingException("@DynamoDBIndexRangeKey must specify RANGE GSI and/or LSI name/names"); + } + } + if (!gsis.isEmpty()) { + return Collections.unmodifiableMap(gsis); + } + return Collections.>emptyMap(); + } + + /** + * {@inheritDoc} + */ + @Override + public List localSecondaryIndexNames() { + DynamoDBIndexRangeKey annotation = actualOf(DynamoDBIndexRangeKey.class); + if (annotation != null) { + if (!annotation.localSecondaryIndexName().isEmpty()) { + if (annotation.localSecondaryIndexNames().length > 0) { + throw new DynamoDBMappingException("@DynamoDBIndexRangeKey must not specify both LSI name/names"); + } + return Collections.singletonList(annotation.localSecondaryIndexName()); + } else if (annotation.localSecondaryIndexNames().length > 0) { + return Collections.unmodifiableList(Arrays.asList(annotation.localSecondaryIndexNames())); + } + } + return Collections.emptyList(); + } + } + + /** + * A map of annotation type to annotation. It will map any first level + * custom annotations to any DynamoDB annotation types that are present. + * It will support up to two levels of compounded DynamoDB annotations. + */ + private static final class Annotations extends LinkedHashMap,Annotation> { + private static final long serialVersionUID = -1L; + + /** + * Puts the annotation if it's DynamoDB; ensures there are no conflicts. + */ + public boolean putIfAnnotated(Class annotationType, Annotation annotation) { + if (!annotationType.isAnnotationPresent(DynamoDB.class)) { + return false; + } else if ((annotation = put(annotationType, annotation)) == null) { + return true; + } + throw new DynamoDBMappingException( + "conflicting annotations " + annotation + " and " + get(annotationType) + + "; allowed only one of @" + annotationType.getSimpleName() + ); + } + + /** + * Puts all DynamoDB annotations and meta-annotations in the map. + */ + public Annotations putAll(Annotation ... annotations) { + for (Annotation a1 : annotations) { + putIfAnnotated(a1.annotationType(), a1); + for (Annotation a2 : a1.annotationType().getAnnotations()) { + if (putIfAnnotated(a2.annotationType(), a1)) { + for (Annotation a3 : a2.annotationType().getAnnotations()) { + putIfAnnotated(a3.annotationType(), a2); + } + } + } + } + return this; + } + } + + /** + * Creates a new instance of the clazz with the target type and annotation + * as parameters if available. + */ + private static T overrideOf(Class clazz, Class targetType, Annotation annotation) { + try { + if (annotation != null) { + try { + return clazz.getConstructor(Class.class, annotation.annotationType()).newInstance(targetType, annotation); + } catch (final NoSuchMethodException no) {} + } + try { + return clazz.getConstructor(Class.class).newInstance(targetType); + } catch (final NoSuchMethodException no) {} + return clazz.newInstance(); + } catch (final Exception e) { + throw new DynamoDBMappingException("could not instantiate " + clazz, e); + } + } + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/StandardBeanProperties.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/StandardBeanProperties.java new file mode 100644 index 000000000000..77092f85c34c --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/StandardBeanProperties.java @@ -0,0 +1,273 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperFieldModel.DynamoDBAttributeType; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperFieldModel.Reflect; +import software.amazon.awssdk.mapper.dynamodb.StandardAnnotationMaps.FieldMap; +import software.amazon.awssdk.mapper.dynamodb.StandardAnnotationMaps.TableMap; +import software.amazon.awssdk.utils.StringUtils; + +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * Reflection assistant for {@link DynamoDBMapper} + */ +@SdkInternalApi +final class StandardBeanProperties { + + private StandardBeanProperties() { + } + + /** + * Returns the bean mappings for a given class (caches the results). + */ + @SuppressWarnings("unchecked") + static final Beans of(Class clazz) { + return ((CachedBeans)CachedBeans.CACHE).getBeans(clazz); + } + + /** + * Cache of {@link Beans} by class type. + */ + private static final class CachedBeans { + private static final CachedBeans CACHE = new CachedBeans(); + private final ConcurrentMap,Beans> cache = new ConcurrentHashMap,Beans>(); + + private final Beans getBeans(Class clazz) { + if (!cache.containsKey(clazz)) { + TableMap annotations = StandardAnnotationMaps.of(clazz); + BeanMap map = new BeanMap(clazz, false); + cache.putIfAbsent(clazz, new Beans(annotations, map)); + } + return cache.get(clazz); + } + } + + /** + * Cache of {@link Bean} mappings by class type. + */ + static final class Beans { + private final DynamoDBMapperTableModel.Properties properties; + private final Map> map; + + private Beans(TableMap annotations, Map> map) { + this.properties = new DynamoDBMapperTableModel.Properties.Immutable(annotations); + this.map = Collections.unmodifiableMap(map); + } + + final DynamoDBMapperTableModel.Properties properties() { + return this.properties; + } + + final Map> map() { + return this.map; + } + } + + /** + * Holds the reflection bean properties for a given property. + */ + static final class Bean { + private final DynamoDBMapperFieldModel.Properties properties; + private final ConvertibleType type; + private final Reflect reflect; + + private Bean(FieldMap annotations, Reflect reflect, Method getter) { + this.properties = new DynamoDBMapperFieldModel.Properties.Immutable(annotations); + this.type = ConvertibleType.of(getter, annotations); + this.reflect = reflect; + } + + final DynamoDBMapperFieldModel.Properties properties() { + return this.properties; + } + + final ConvertibleType type() { + return this.type; + } + + final Reflect reflect() { + return this.reflect; + } + } + + /** + * Get/set reflection operations. + */ + static final class MethodReflect implements Reflect { + private final Method getter, setter; + + private MethodReflect(Method getter) { + this.setter = setterOf(getter); + this.getter = getter; + } + + @Override + public V get(T object) { + try { + return (V)getter.invoke(object); + } catch (final Exception e) { + throw new DynamoDBMappingException("could not invoke " + getter + " on " + object.getClass(), e); + } + } + + @Override + public void set(T object, V value) { + try { + setter.invoke(object, value); + } catch (final Exception e) { + throw new DynamoDBMappingException("could not invoke " + setter + " on " + object.getClass() + + " with value " + value + " of type " + (value == null ? null : value.getClass()), e); + } + } + + static Method setterOf(Method getter) { + try { + String name = "set" + getter.getName().replaceFirst("^(get|is)",""); + return getter.getDeclaringClass().getMethod(name, getter.getReturnType()); + } catch (final Exception no) {} + return null; + } + } + + /** + * Get/set reflection operations with a declaring property. + */ + static final class DeclaringReflect implements Reflect { + private final Reflect reflect; + private final Reflect declaring; + private final Class targetType; + + private DeclaringReflect(Method getter, Reflect declaring, Class targetType) { + this.reflect = new MethodReflect(getter); + this.declaring = declaring; + this.targetType = targetType; + } + + @Override + public V get(T object) { + T declaringObject = declaring.get(object); + if (declaringObject == null) { + return null; + } + return reflect.get(declaringObject); + } + + @Override + public void set(T object, V value) { + T declaringObject = declaring.get(object); + if (declaringObject == null) { + declaring.set(object, (declaringObject = newInstance(targetType))); + } + reflect.set(declaringObject, value); + } + + static T newInstance(Class targetType) { + try { + return targetType.newInstance(); + } catch (final Exception e) { + throw new DynamoDBMappingException("could not instantiate " + targetType, e); + } + } + } + + /** + * {@link Map} of {@link Bean} + */ + static final class BeanMap extends LinkedHashMap> { + private final Class clazz; + + BeanMap(Class clazz, boolean inherited) { + this.clazz = clazz; + putAll(clazz, inherited); + } + + private void putAll(Class clazz, boolean inherited) { + for (Method method : clazz.getMethods()) { + if (canMap(method, inherited)) { + FieldMap annotations = StandardAnnotationMaps.of(method, null); + if (!annotations.ignored()) { + Reflect reflect = new MethodReflect(method); + putOrFlatten(annotations, reflect, method); + } + } + } + } + + private void putOrFlatten(FieldMap annotations, Reflect reflect, Method getter) { + if (annotations.flattened()) { + flatten((Class)annotations.targetType(), annotations.attributes(), (Reflect)reflect); + } else { + Bean bean = new Bean(annotations, reflect, getter); + if (put(bean.properties().attributeName(), bean) != null) { + throw new DynamoDBMappingException("duplicate attribute name " + bean.properties().attributeName()); + } + } + } + + private void flatten(Class targetType, Map attributes, Reflect declaring) { + for (Method method : targetType.getMethods()) { + if (canMap(method, true)) { + String name = fieldNameOf(method); + if ((name = attributes.remove(name)) == null) { + continue; + } + FieldMap annotations = StandardAnnotationMaps.of(method, name); + if (!annotations.ignored()) { + Reflect reflect = new DeclaringReflect(method, declaring, targetType); + putOrFlatten(annotations, reflect, method); + } + } + } + if (!attributes.isEmpty()) { //<- this should be empty by now + throw new DynamoDBMappingException("contains unknown flattened attribute(s): " + attributes); + } + } + + private boolean canMap(Method method, boolean inherited) { + if (method.getName().matches("^(get|is).+") == false) { + return false; + } else if (method.getParameterTypes().length != 0) { + return false; + } else if (method.isBridge() || method.isSynthetic()) { + return false; + } else if (method.getDeclaringClass() == Object.class) { + return false; + } else if (!inherited && method.getDeclaringClass() != this.clazz && + StandardAnnotationMaps.of(method.getDeclaringClass()).attributeType() == null) { + return false; + } else { + return true; + } + } + } + + /** + * Gets the field name given the getter method. + */ + static final String fieldNameOf(Method getter) { + String name = getter.getName().replaceFirst("^(get|is)",""); + return StringUtils.lowerCase(name.substring(0, 1)) + name.substring(1); + } + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/StandardModelFactories.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/StandardModelFactories.java new file mode 100644 index 000000000000..2887b22d2500 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/StandardModelFactories.java @@ -0,0 +1,760 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.mapper.dynamodb.internal.MapperBinaryUtils; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperFieldModel.DynamoDBAttributeType; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperFieldModel.Reflect; +import software.amazon.awssdk.mapper.dynamodb.internal.DynamoDBMapperModelFactory; +import software.amazon.awssdk.mapper.dynamodb.internal.DynamoDBMapperModelFactory.TableFactory; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTypeConverter.AbstractConverter; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTypeConverter.DelegateConverter; +import software.amazon.awssdk.mapper.dynamodb.StandardBeanProperties.Bean; +import software.amazon.awssdk.mapper.dynamodb.StandardBeanProperties.Beans; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +import software.amazon.awssdk.utils.Logger; +import org.joda.time.DateTime; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collection; +import java.util.Date; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import static software.amazon.awssdk.mapper.dynamodb.StandardTypeConverters.Scalar.BOOLEAN; +import static software.amazon.awssdk.mapper.dynamodb.StandardTypeConverters.Scalar.DEFAULT; +import static software.amazon.awssdk.mapper.dynamodb.StandardTypeConverters.Scalar.STRING; +import static software.amazon.awssdk.mapper.dynamodb.StandardTypeConverters.Vector.LIST; +import static software.amazon.awssdk.mapper.dynamodb.StandardTypeConverters.Vector.MAP; +import static software.amazon.awssdk.mapper.dynamodb.StandardTypeConverters.Vector.SET; +import static software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType.B; +import static software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType.N; +import static software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType.S; + +/** + * Pre-defined strategies for mapping between Java types and DynamoDB types. + */ +@SdkInternalApi +final class StandardModelFactories { + + private StandardModelFactories() { + } + + private static final Logger LOG = Logger.loggerFor(StandardModelFactories.class); + + /** + * Creates the standard {@link DynamoDBMapperModelFactory} factory. + */ + static final DynamoDBMapperModelFactory of(S3Link.Factory s3Links) { + return new StandardModelFactory(s3Links); + } + + /** + * {@link TableFactory} mapped by {@link ConversionSchema}. + */ + private static final class StandardModelFactory implements DynamoDBMapperModelFactory { + private final ConcurrentMap cache; + private final S3Link.Factory s3Links; + + private StandardModelFactory(S3Link.Factory s3Links) { + this.cache = new ConcurrentHashMap(); + this.s3Links = s3Links; + } + + @Override + public TableFactory getTableFactory(DynamoDBMapperConfig config) { + ConversionSchema schema = config.getConversionSchema(); + if (!cache.containsKey(schema)) { + RuleFactory rules = rulesOf(config, s3Links, this); + rules = new ConversionSchemas.ItemConverterRuleFactory(config, s3Links, rules); + cache.putIfAbsent(schema, new StandardTableFactory(rules)); + } + return cache.get(schema); + } + } + + /** + * {@link DynamoDBMapperTableModel} mapped by the clazz. + */ + private static final class StandardTableFactory implements TableFactory { + private final ConcurrentMap,DynamoDBMapperTableModel> cache; + private final RuleFactory rules; + + private StandardTableFactory(RuleFactory rules) { + this.cache = new ConcurrentHashMap,DynamoDBMapperTableModel>(); + this.rules = rules; + } + + @Override + @SuppressWarnings("unchecked") + public DynamoDBMapperTableModel getTable(Class clazz) { + if (!this.cache.containsKey(clazz)) { + this.cache.putIfAbsent(clazz, new TableBuilder(clazz, rules).build()); + } + return (DynamoDBMapperTableModel)this.cache.get(clazz); + } + } + + /** + * {@link DynamoDBMapperTableModel} builder. + */ + private static final class TableBuilder extends DynamoDBMapperTableModel.Builder { + private TableBuilder(Class clazz, Beans beans, RuleFactory rules) { + super(clazz, beans.properties()); + for (Bean bean : beans.map().values()) { + try { + with(new FieldBuilder(clazz, bean, rules.getRule(bean.type())).build()); + } catch (final RuntimeException e) { + throw new DynamoDBMappingException(String.format( + "%s[%s] could not be mapped for type %s", + clazz.getSimpleName(), bean.properties().attributeName(), bean.type() + ), e); + } + } + } + + private TableBuilder(Class clazz, RuleFactory rules) { + this(clazz, StandardBeanProperties.of(clazz), rules); + } + } + + /** + * {@link DynamoDBMapperFieldModel} builder. + */ + private static final class FieldBuilder extends DynamoDBMapperFieldModel.Builder { + private FieldBuilder(Class clazz, Bean bean, Rule rule) { + super(clazz, bean.properties()); + if (bean.type().attributeType() != null) { + with(bean.type().attributeType()); + } else { + with(rule.getAttributeType()); + } + with(rule.newConverter(bean.type())); + with(bean.reflect()); + } + } + + /** + * Creates a new set of conversion rules based on the configuration. + */ + private static final RuleFactory rulesOf(DynamoDBMapperConfig config, S3Link.Factory s3Links, DynamoDBMapperModelFactory models) { + boolean ver1 = (config.getConversionSchema() == ConversionSchemas.V1); + boolean ver2 = (config.getConversionSchema() == ConversionSchemas.V2); + boolean v2Compatible = (config.getConversionSchema() == ConversionSchemas.V2_COMPATIBLE); + + DynamoDBTypeConverterFactory.Builder scalars = config.getTypeConverterFactory().override(); + scalars.with(String.class, S3Link.class, s3Links); + + Rules factory = new Rules(scalars.build()); + factory.add(factory.new NativeType(!ver1)); + factory.add(factory.new V2CompatibleBool(v2Compatible)); + factory.add(factory.new NativeBool(ver2)); + factory.add(factory.new StringScalar(true)); + factory.add(factory.new DateToEpochRule(true)); + factory.add(factory.new NumberScalar(true)); + factory.add(factory.new BinaryScalar(true)); + factory.add(factory.new NativeBoolSet(ver2)); + factory.add(factory.new StringScalarSet(true)); + factory.add(factory.new NumberScalarSet(true)); + factory.add(factory.new BinaryScalarSet(true)); + factory.add(factory.new ObjectSet(ver2)); + factory.add(factory.new ObjectStringSet(!ver2)); + factory.add(factory.new ObjectList(!ver1)); + factory.add(factory.new ObjectMap(!ver1)); + factory.add(factory.new ObjectDocumentMap(!ver1, models, config)); + return factory; + } + + /** + * Groups the conversion rules to be evaluated. + */ + private static final class Rules implements RuleFactory { + private final Set> rules = new LinkedHashSet>(); + private final DynamoDBTypeConverterFactory scalars; + + private Rules(DynamoDBTypeConverterFactory scalars) { + this.scalars = scalars; + } + + @SuppressWarnings("unchecked") + private void add(Rule rule) { + this.rules.add((Rule)rule); + } + + @Override + public Rule getRule(ConvertibleType type) { + for (Rule rule : rules) { + if (rule.isAssignableFrom(type)) { + return rule; + } + } + return new NotSupported(); + } + + /** + * Native {@link AttributeValue} conversion. + */ + private class NativeType extends AbstractRule { + private NativeType(boolean supported) { + super(DynamoDBAttributeType.NULL, supported); + } + @Override + public boolean isAssignableFrom(ConvertibleType type) { + return super.supported && type.is(AttributeValue.class); + } + @Override + public DynamoDBTypeConverter newConverter(ConvertibleType type) { + return joinAll(type.typeConverter()); + } + @Override + public AttributeValue get(AttributeValue o) { + return o; + } + @Override + public AttributeValue build(AttributeValue o) { + return o; + } + } + + /** + * {@code S} conversion + */ + private class StringScalar extends AbstractRule { + private StringScalar(boolean supported) { + super(DynamoDBAttributeType.S, supported); + } + @Override + public boolean isAssignableFrom(ConvertibleType type) { + return super.isAssignableFrom(type) && (type.attributeType() != null || type.is(S)); + } + @Override + public DynamoDBTypeConverter newConverter(ConvertibleType type) { + return joinAll(getConverter(String.class, type), type.typeConverter()); + } + @Override + public String get(AttributeValue value) { + return value.s(); + } + @Override + public AttributeValue build(String o) { + return AttributeValue.builder().s(o).build(); + } + @Override + public AttributeValue convert(String o) { + return o.length() == 0 ? null : super.convert(o); + } + } + + /** + * {@code N} conversion + */ + private class NumberScalar extends AbstractRule { + private NumberScalar(boolean supported) { + super(DynamoDBAttributeType.N, supported); + } + @Override + public boolean isAssignableFrom(ConvertibleType type) { + return super.isAssignableFrom(type) && (type.attributeType() != null || type.is(N)); + } + @Override + public DynamoDBTypeConverter newConverter(ConvertibleType type) { + return joinAll(getConverter(String.class, type), type.typeConverter()); + } + @Override + public String get(AttributeValue value) { + return value.n(); + } + @Override + public AttributeValue build(String o) { + return AttributeValue.builder().n(o).build(); + } + } + + /** + * {@code N} conversion + */ + private class DateToEpochRule extends AbstractRule { + private DateToEpochRule(boolean supported) { + super(DynamoDBAttributeType.N, supported); + } + @Override + public boolean isAssignableFrom(ConvertibleType type) { + return (type.is(Date.class) || type.is(Calendar.class) || type.is(DateTime.class)) + && super.isAssignableFrom(type) && (type.attributeType() != null || type.is(N)); + } + @Override + public DynamoDBTypeConverter newConverter(ConvertibleType type) { + return joinAll(getConverter(Long.class, type), type.typeConverter()); + } + @Override + public Long get(AttributeValue value) { + return Long.valueOf(value.n()); + } + @Override + public AttributeValue build(Long o) { + return AttributeValue.builder().n(String.valueOf(o)).build(); + } + } + + /** + * {@code B} conversion + */ + private class BinaryScalar extends AbstractRule { + private BinaryScalar(boolean supported) { + super(DynamoDBAttributeType.B, supported); + } + @Override + public boolean isAssignableFrom(ConvertibleType type) { + return super.isAssignableFrom(type) && (type.attributeType() != null || type.is(B)); + } + @Override + public DynamoDBTypeConverter newConverter(ConvertibleType type) { + return joinAll(getConverter(ByteBuffer.class, type), type.typeConverter()); + } + @Override + public ByteBuffer get(AttributeValue value) { + return MapperBinaryUtils.toWritableByteBuffer(value.b()); + } + @Override + public AttributeValue build(ByteBuffer o) { + return AttributeValue.builder().b(SdkBytes.fromByteBuffer(o)).build(); + } + } + + /** + * {@code SS} conversion + */ + private class StringScalarSet extends AbstractRule,Collection> { + private StringScalarSet(boolean supported) { + super(DynamoDBAttributeType.SS, supported); + } + @Override + public boolean isAssignableFrom(ConvertibleType type) { + return super.isAssignableFrom(type) && (type.attributeType() != null || type.is(S, SET)); + } + @Override + public DynamoDBTypeConverter> newConverter(ConvertibleType> type) { + return joinAll(SET.join(getConverter(String.class, type.param(0))), type.>typeConverter()); + } + @Override + public List get(AttributeValue value) { + return value.hasSs() ? value.ss() : null; + } + @Override + public AttributeValue build(List o) { + return AttributeValue.builder().ss(o).build(); + } + } + + /** + * {@code NS} conversion + */ + private class NumberScalarSet extends AbstractRule,Collection> { + private NumberScalarSet(boolean supported) { + super(DynamoDBAttributeType.NS, supported); + } + @Override + public boolean isAssignableFrom(ConvertibleType type) { + return super.isAssignableFrom(type) && (type.attributeType() != null || type.is(N, SET)); + } + @Override + public DynamoDBTypeConverter> newConverter(ConvertibleType> type) { + return joinAll(SET.join(getConverter(String.class, type.param(0))), type.>typeConverter()); + } + @Override + public List get(AttributeValue value) { + return value.hasNs() ? value.ns() : null; + } + @Override + public AttributeValue build(List o) { + return AttributeValue.builder().ns(o).build(); + } + } + + /** + * {@code BS} conversion + */ + private class BinaryScalarSet extends AbstractRule,Collection> { + private BinaryScalarSet(boolean supported) { + super(DynamoDBAttributeType.BS, supported); + } + @Override + public boolean isAssignableFrom(ConvertibleType type) { + return super.isAssignableFrom(type) && (type.attributeType() != null || type.is(B, SET)); + } + @Override + public DynamoDBTypeConverter> newConverter(ConvertibleType> type) { + return joinAll(SET.join(getConverter(ByteBuffer.class, type.param(0))), type.>typeConverter()); + } + @Override + public List get(AttributeValue value) { + if (!value.hasBs()) { + return null; + } + List result = new ArrayList(value.bs().size()); + for (SdkBytes sb : value.bs()) { + result.add(MapperBinaryUtils.toWritableByteBuffer(sb)); + } + return result; + } + @Override + public AttributeValue build(List o) { + List sdkBytes = + new ArrayList(o.size()); + for (ByteBuffer bb : o) { + sdkBytes.add(SdkBytes.fromByteBuffer(bb)); + } + return AttributeValue.builder().bs(sdkBytes).build(); + } + } + + /** + * {@code SS} conversion + */ + private class ObjectStringSet extends StringScalarSet { + private ObjectStringSet(boolean supported) { + super(supported); + } + @Override + public boolean isAssignableFrom(ConvertibleType type) { + return type.attributeType() == null && super.supported && type.is(SET); + } + @Override + public DynamoDBTypeConverter> newConverter(ConvertibleType> type) { + LOG.warn(() -> "Marshaling a set of non-String objects to a DynamoDB " + + "StringSet. You won't be able to read these objects back " + + "out of DynamoDB unless you REALLY know what you're doing: " + + "it's probably a bug. If you DO know what you're doing feel" + + "free to ignore this warning, but consider using a custom " + + "marshaler for this instead."); + return joinAll(SET.join(scalars.getConverter(String.class, DEFAULT.type())), type.>typeConverter()); + } + } + + /** + * Native boolean conversion. + */ + private class NativeBool extends AbstractRule { + private NativeBool(boolean supported) { + super(DynamoDBAttributeType.BOOL, supported); + } + @Override + public boolean isAssignableFrom(ConvertibleType type) { + return super.isAssignableFrom(type) && type.is(BOOLEAN); + } + @Override + public DynamoDBTypeConverter newConverter(ConvertibleType type) { + return joinAll(getConverter(Boolean.class, type), type.typeConverter()); + } + @Override + public Boolean get(AttributeValue o) { + return o.bool(); + } + @Override + public AttributeValue build(Boolean value) { + return AttributeValue.builder().bool(value).build(); + } + @Override + public Boolean unconvert(AttributeValue o) { + if (o.bool() == null && o.n() != null) { + return BOOLEAN.convert(o.n()); + } + return super.unconvert(o); + } + } + + /** + * Native boolean conversion. + */ + private class V2CompatibleBool extends AbstractRule { + private V2CompatibleBool(boolean supported) { + super(DynamoDBAttributeType.N, supported); + } + + @Override + public boolean isAssignableFrom(ConvertibleType type) { + return super.isAssignableFrom(type) && type.is(BOOLEAN); + } + + @Override + public DynamoDBTypeConverter newConverter(ConvertibleType type) { + return joinAll(getConverter(String.class, type), type.typeConverter()); + } + + /** + * For V2 Compatible schema we support loading booleans from a numeric attribute value (0/1) or the native boolean + * type. + */ + @Override + public String get(AttributeValue o) { + if(o.bool() != null) { + // Handle native bools, transform to expected numeric representation. + return o.bool() ? "1" : "0"; + } + return o.n(); + } + + /** + * For the V2 compatible schema we save as a numeric attribute value unless overridden by {@link + * DynamoDBNativeBoolean} or {@link DynamoDBTyped}. + */ + @Override + public AttributeValue build(String value) { + return AttributeValue.builder().n(value).build(); + } + } + + /** + * Any {@link Set} conversions. + */ + private class ObjectSet extends AbstractRule,Collection> { + private ObjectSet(boolean supported) { + super(DynamoDBAttributeType.L, supported); + } + @Override + public boolean isAssignableFrom(ConvertibleType type) { + return super.isAssignableFrom(type) && type.param(0) != null && type.is(SET); + } + @Override + public DynamoDBTypeConverter> newConverter(ConvertibleType> type) { + return joinAll(SET.join(getConverter(type.param(0))), type.>typeConverter()); + } + @Override + public List get(AttributeValue value) { + return value.hasL() ? value.l() : null; + } + @Override + public AttributeValue build(List o) { + return AttributeValue.builder().l(o).build(); + } + } + + /** + * Native bool {@link Set} conversions. + */ + private class NativeBoolSet extends ObjectSet { + private NativeBoolSet(boolean supported) { + super(supported); + } + @Override + public boolean isAssignableFrom(ConvertibleType type) { + return super.isAssignableFrom(type) && type.param(0).is(BOOLEAN); + } + @Override + public List unconvert(AttributeValue o) { + if (!o.hasL() && o.hasNs()) { + return LIST.convert(o.ns(), new NativeBool(true).join(scalars.getConverter(Boolean.class, String.class))); + } + return super.unconvert(o); + } + } + + /** + * Any {@link List} conversions. + */ + private class ObjectList extends AbstractRule,List> { + private ObjectList(boolean supported) { + super(DynamoDBAttributeType.L, supported); + } + @Override + public boolean isAssignableFrom(ConvertibleType type) { + return super.isAssignableFrom(type) && type.param(0) != null && type.is(LIST); + } + @Override + public DynamoDBTypeConverter> newConverter(ConvertibleType> type) { + return joinAll(LIST.join(getConverter(type.param(0))), type.>typeConverter()); + } + @Override + public List get(AttributeValue value) { + return value.hasL() ? value.l() : null; + } + @Override + public AttributeValue build(List o) { + return AttributeValue.builder().l(o).build(); + } + } + + /** + * Any {@link Map} conversions. + */ + private class ObjectMap extends AbstractRule,Map> { + private ObjectMap(boolean supported) { + super(DynamoDBAttributeType.M, supported); + } + @Override + public boolean isAssignableFrom(ConvertibleType type) { + return super.isAssignableFrom(type) && type.param(1) != null && type.is(MAP) && type.param(0).is(STRING); + } + @Override + public DynamoDBTypeConverter> newConverter(ConvertibleType> type) { + return joinAll( + MAP.join(getConverter(type.param(1))), + type.>typeConverter() + ); + } + @Override + public Map get(AttributeValue value) { + return value.hasM() ? value.m() : null; + } + @Override + public AttributeValue build(Map o) { + return AttributeValue.builder().m(o).build(); + } + } + + /** + * All object conversions. + */ + private class ObjectDocumentMap extends AbstractRule,T> { + private final DynamoDBMapperModelFactory models; + private final DynamoDBMapperConfig config; + private ObjectDocumentMap(boolean supported, DynamoDBMapperModelFactory models, DynamoDBMapperConfig config) { + super(DynamoDBAttributeType.M, supported); + this.models = models; + this.config = config; + } + @Override + public boolean isAssignableFrom(ConvertibleType type) { + return type.attributeType() == getAttributeType() && super.supported && !type.is(MAP); + } + @Override + public DynamoDBTypeConverter newConverter(final ConvertibleType type) { + return joinAll(new DynamoDBTypeConverter,T>() { + public final Map convert(final T o) { + return models.getTableFactory(config).getTable(type.targetType()).convert(o); + } + public final T unconvert(final Map o) { + return models.getTableFactory(config).getTable(type.targetType()).unconvert(o); + } + }, type.>typeConverter()); + } + @Override + public Map get(AttributeValue value) { + return value.hasM() ? value.m() : null; + } + @Override + public AttributeValue build(Map o) { + return AttributeValue.builder().m(o).build(); + } + } + + /** + * Default conversion when no match could be determined. + */ + private class NotSupported extends AbstractRule { + private NotSupported() { + super(DynamoDBAttributeType.NULL, false); + } + @Override + public DynamoDBTypeConverter newConverter(ConvertibleType type) { + return this; + } + @Override + public T get(AttributeValue value) { + throw new DynamoDBMappingException("not supported; requires @DynamoDBTyped or @DynamoDBTypeConverted"); + } + @Override + public AttributeValue build(T o) { + throw new DynamoDBMappingException("not supported; requires @DynamoDBTyped or @DynamoDBTypeConverted"); + } + } + + /** + * Gets the scalar converter for the given source and target types. + */ + private DynamoDBTypeConverter getConverter(Class sourceType, ConvertibleType type) { + return scalars.getConverter(sourceType, type.targetType()); + } + + /** + * Gets the nested converter for the given conversion type. + * Also wraps the resulting converter with a nullable converter. + */ + private DynamoDBTypeConverter getConverter(ConvertibleType type) { + return new DelegateConverter(getRule(type).newConverter(type)) { + public final AttributeValue convert(T o) { + return o == null ? AttributeValue.builder().nul(true).build() : super.convert(o); + } + }; + } + } + + /** + * Basic attribute value conversion functions. + */ + private static abstract class AbstractRule extends AbstractConverter implements Rule { + protected final DynamoDBAttributeType attributeType; + protected final boolean supported; + protected AbstractRule(DynamoDBAttributeType attributeType, boolean supported) { + this.attributeType = attributeType; + this.supported = supported; + } + @Override + public boolean isAssignableFrom(ConvertibleType type) { + return type.attributeType() == null ? supported : type.attributeType() == attributeType; + } + @Override + public DynamoDBAttributeType getAttributeType() { + return this.attributeType; + } + @Override + public AttributeValue convert(final S o) { + return build(o); + } + /** + * Builds an immutable v2 AttributeValue from the given value. + */ + public abstract AttributeValue build(S o); + /** + * Extracts the value from an AttributeValue. + */ + public abstract S get(AttributeValue o); + @Override + public S unconvert(final AttributeValue o) { + S value = get(o); + if (value == null && o.nul() == null) { + throw new DynamoDBMappingException("expected " + attributeType + " in value " + o); + } + return value; + } + } + + /** + * Attribute value conversion. + */ + static interface Rule { + boolean isAssignableFrom(ConvertibleType type); + DynamoDBTypeConverter newConverter(ConvertibleType type); + DynamoDBAttributeType getAttributeType(); + } + + /** + * Attribute value conversion factory. + */ + static interface RuleFactory { + Rule getRule(ConvertibleType type); + } + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/StandardTypeConverters.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/StandardTypeConverters.java new file mode 100644 index 000000000000..db457a26be1d --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/StandardTypeConverters.java @@ -0,0 +1,998 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.mapper.dynamodb.internal.MapperDateUtils; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.AbstractMap.SimpleImmutableEntry; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Calendar; +import java.util.Currency; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TimeZone; +import java.util.regex.Pattern; + +import org.joda.time.DateTime; + +/** + * Type conversions. + * + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBTypeConverter + */ +@SdkInternalApi +final class StandardTypeConverters extends DynamoDBTypeConverterFactory { + + /** + * Standard scalar type-converter factory. + */ + private static final DynamoDBTypeConverterFactory FACTORY = new StandardTypeConverters(); + static DynamoDBTypeConverterFactory factory() { + return StandardTypeConverters.FACTORY; + } + + /** + * {@inheritDoc} + */ + @Override + public DynamoDBTypeConverter getConverter(Class sourceType, Class targetType) { + Scalar source = Scalar.of(sourceType), target = Scalar.of(targetType); + Converter toSource = source.getConverter(sourceType, target.type()); + Converter toTarget = target.getConverter(targetType, source.type()); + return new DynamoDBTypeConverter() { + @Override + public final S convert(final T o) { + return toSource.convert(o); + } + @Override + public final T unconvert(final S o) { + return toTarget.convert(o); + } + }; + } + + /** + * Standard scalar types. + */ + static enum Scalar { + /** + * {@link BigDecimal} + */ + BIG_DECIMAL(ScalarAttributeType.N, new ConverterMap(BigDecimal.class, null) + .with(Number.class, ToBigDecimal.FromString.join(ToString.FromNumber)) + .with(String.class, ToBigDecimal.FromString) + ), + + /** + * {@link BigInteger} + */ + BIG_INTEGER(ScalarAttributeType.N, new ConverterMap(BigInteger.class, null) + .with(Number.class, ToBigInteger.FromString.join(ToString.FromNumber)) + .with(String.class, ToBigInteger.FromString) + ), + + /** + * {@link Boolean} + */ + BOOLEAN(ScalarAttributeType.N, new ConverterMap(Boolean.class, Boolean.TYPE) + .with(Number.class, ToBoolean.FromString.join(ToString.FromNumber)) + .with(String.class, ToBoolean.FromString) + ), + + /** + * {@link Byte} + */ + BYTE(ScalarAttributeType.N, new ConverterMap(Byte.class, Byte.TYPE) + .with(Number.class, ToByte.FromNumber) + .with(String.class, ToByte.FromString) + ), + + /** + * {@link Byte} array + */ + BYTE_ARRAY(ScalarAttributeType.B, new ConverterMap(byte[].class, null) + .with(ByteBuffer.class, ToByteArray.FromByteBuffer) + .with(String.class, ToByteArray.FromString) + ), + + /** + * {@link ByteBuffer} + */ + BYTE_BUFFER(ScalarAttributeType.B, new ConverterMap(ByteBuffer.class, null) + .with(byte[].class, ToByteBuffer.FromByteArray) + .with(String.class, ToByteBuffer.FromByteArray.join(ToByteArray.FromString)) + .with(java.util.UUID.class, ToByteBuffer.FromUuid) + ), + + /** + * {@link Calendar} + */ + CALENDAR(ScalarAttributeType.S, new ConverterMap(Calendar.class, null) + .with(Date.class, ToCalendar.FromDate) + .with(DateTime.class, ToCalendar.FromDate.join(ToDate.FromDateTime)) + .with(Long.class, ToCalendar.FromDate.join(ToDate.FromLong)) + .with(String.class, ToCalendar.FromDate.join(ToDate.FromString)) + ), + + /** + * {@link Character} + */ + CHARACTER(ScalarAttributeType.S, new ConverterMap(Character.class, Character.TYPE) + .with(String.class, ToCharacter.FromString) + ), + + /** + * {@link Currency} + */ + CURRENCY(ScalarAttributeType.S, new ConverterMap(Currency.class, null) + .with(String.class, ToCurrency.FromString) + ), + + /** + * {@link Date} + */ + DATE(ScalarAttributeType.S, new ConverterMap(Date.class, null) + .with(Calendar.class, ToDate.FromCalendar) + .with(DateTime.class, ToDate.FromDateTime) + .with(Long.class, ToDate.FromLong) + .with(String.class, ToDate.FromString) + ), + + /** + * {@link DateTime} + */ + DATE_TIME(/*ScalarAttributeType.S*/null, new ConverterMap(DateTime.class, null) + .with(Calendar.class, ToDateTime.FromDate.join(ToDate.FromCalendar)) + .with(Date.class, ToDateTime.FromDate) + .with(Long.class, ToDateTime.FromDate.join(ToDate.FromLong)) + .with(String.class, ToDateTime.FromDate.join(ToDate.FromString)) + ), + + /** + * {@link Double} + */ + DOUBLE(ScalarAttributeType.N, new ConverterMap(Double.class, Double.TYPE) + .with(Number.class, ToDouble.FromNumber) + .with(String.class, ToDouble.FromString) + ), + + /** + * {@link Float} + */ + FLOAT(ScalarAttributeType.N, new ConverterMap(Float.class, Float.TYPE) + .with(Number.class, ToFloat.FromNumber) + .with(String.class, ToFloat.FromString) + ), + + /** + * {@link Integer} + */ + INTEGER(ScalarAttributeType.N, new ConverterMap(Integer.class, Integer.TYPE) + .with(Number.class, ToInteger.FromNumber) + .with(String.class, ToInteger.FromString) + ), + + /** + * {@link Locale} + */ + LOCALE(ScalarAttributeType.S, new ConverterMap(Locale.class, null) + .with(String.class, ToLocale.FromString) + ), + + /** + * {@link Long} + */ + LONG(ScalarAttributeType.N, new ConverterMap(Long.class, Long.TYPE) + .with(Date.class, ToLong.FromDate) + .with(DateTime.class, ToLong.FromDate.join(ToDate.FromDateTime)) + .with(Calendar.class, ToLong.FromDate.join(ToDate.FromCalendar)) + .with(Number.class, ToLong.FromNumber) + .with(String.class, ToLong.FromString) + ), + + /** + * {@link S3Link} + */ + S3_LINK(ScalarAttributeType.S, new ConverterMap(S3Link.class, null)), + + /** + * {@link Short} + */ + SHORT(ScalarAttributeType.N, new ConverterMap(Short.class, Short.TYPE) + .with(Number.class, ToShort.FromNumber) + .with(String.class, ToShort.FromString) + ), + + /** + * {@link String} + */ + STRING(ScalarAttributeType.S, new ConverterMap(String.class, null) + .with(Boolean.class, ToString.FromBoolean) + .with(byte[].class, ToString.FromByteArray) + .with(ByteBuffer.class, ToString.FromByteArray.join(ToByteArray.FromByteBuffer)) + .with(Calendar.class, ToString.FromDate.join(ToDate.FromCalendar)) + .with(Date.class, ToString.FromDate) + .with(Enum.class, ToString.FromEnum) + .with(Locale.class, ToString.FromLocale) + .with(TimeZone.class, ToString.FromTimeZone) + .with(Object.class, ToString.FromObject) + ), + + /** + * {@link TimeZone} + */ + TIME_ZONE(ScalarAttributeType.S, new ConverterMap(TimeZone.class, null) + .with(String.class, ToTimeZone.FromString) + ), + + /** + * {@link java.net.URL} + */ + URL(ScalarAttributeType.S, new ConverterMap(java.net.URL.class, null) + .with(String.class, ToUrl.FromString) + ), + + /** + * {@link java.net.URI} + */ + URI(ScalarAttributeType.S, new ConverterMap(java.net.URI.class, null) + .with(String.class, ToUri.FromString) + ), + + /** + * {@link java.util.UUID} + */ + UUID(ScalarAttributeType.S, new ConverterMap(java.util.UUID.class, null) + .with(ByteBuffer.class, ToUuid.FromByteBuffer) + .with(String.class, ToUuid.FromString) + ), + + /** + * {@link Object}; default must be last + */ + DEFAULT(null, new ConverterMap(Object.class, null)) { + @Override + Converter getConverter(Class sourceType, Class targetType) { + if (sourceType.isEnum() && STRING.map.isAssignableFrom(targetType)) { + return (Converter)new ToEnum.FromString(sourceType); + } + return super.getConverter(sourceType, targetType); + } + }; + + /** + * The scalar attribute type. + */ + private final ScalarAttributeType scalarAttributeType; + + /** + * The mapping of conversion functions for this scalar. + */ + private final ConverterMap map; + + /** + * Constructs a new scalar with the specified conversion mappings. + */ + private Scalar(ScalarAttributeType scalarAttributeType, ConverterMap map) { + this.scalarAttributeType = scalarAttributeType; + this.map = map; + } + + /** + * Returns the function to convert from the specified target class to + * this scalar type. + */ + Converter getConverter(Class sourceType, Class targetType) { + return map.getConverter(targetType); + } + + /** + * Converts the target instance using the standard type-conversions. + */ + @SuppressWarnings("unchecked") + final S convert(Object o) { + return getConverter(this.type(), (Class)o.getClass()).convert(o); + } + + /** + * Determines if the scalar is of the specified scalar attribute type. + */ + final boolean is(final ScalarAttributeType scalarAttributeType) { + return this.scalarAttributeType == scalarAttributeType; + } + + /** + * Determines if the class represented by this scalar is either the + * same as or a supertype of the specified target type. + */ + final boolean is(final Class type) { + return this.map.isAssignableFrom(type); + } + + /** + * Returns the primary reference type. + */ + @SuppressWarnings("unchecked") + final Class type() { + return (Class)this.map.referenceType; + } + + /** + * Returns the first matching scalar, which may be the same as or a + * supertype of the specified target class. + */ + static Scalar of(Class type) { + for (Scalar scalar : Scalar.values()) { + if (scalar.is(type)) { + return scalar; + } + } + return DEFAULT; + } + } + + /** + * Standard vector types. + */ + static abstract class Vector { + /** + * {@link List} + */ + static final ToList LIST = new ToList(); + static final class ToList extends Vector { + DynamoDBTypeConverter,List> join(final DynamoDBTypeConverter scalar) { + return new DynamoDBTypeConverter,List>() { + @Override + public final List convert(final List o) { + return LIST.convert(o, scalar); + } + @Override + public final List unconvert(final List o) { + return LIST.unconvert(o, scalar); + } + }; + } + + List convert(Collection o, DynamoDBTypeConverter scalar) { + List vector = new ArrayList(o.size()); + for (T t : o) { + vector.add(scalar.convert(t)); + } + return vector; + } + + List unconvert(Collection o, DynamoDBTypeConverter scalar) { + List vector = new ArrayList(o.size()); + for (S s : o) { + vector.add(scalar.unconvert(s)); + } + return vector; + } + + @Override + boolean is(final Class type) { + return List.class.isAssignableFrom(type); + } + } + + /** + * {@link Map} + */ + static final ToMap MAP = new ToMap(); + static final class ToMap extends Vector { + DynamoDBTypeConverter,Map> join(final DynamoDBTypeConverter scalar) { + return new DynamoDBTypeConverter,Map>() { + @Override + public final Map convert(final Map o) { + return MAP.convert(o, scalar); + } + @Override + public final Map unconvert(final Map o) { + return MAP.unconvert(o, scalar); + } + }; + } + + Map convert(Map o, DynamoDBTypeConverter scalar) { + Map vector = new LinkedHashMap(); + for (Map.Entry t : o.entrySet()) { + vector.put(t.getKey(), scalar.convert(t.getValue())); + } + return vector; + } + + Map unconvert(Map o, DynamoDBTypeConverter scalar) { + Map vector = new LinkedHashMap(); + for (Map.Entry s : o.entrySet()) { + vector.put(s.getKey(), scalar.unconvert(s.getValue())); + } + return vector; + } + + boolean is(final Class type) { + return Map.class.isAssignableFrom(type); + } + } + + /** + * {@link Set} + */ + static final ToSet SET = new ToSet(); + static final class ToSet extends Vector { + DynamoDBTypeConverter,Collection> join(final DynamoDBTypeConverter target) { + return new DynamoDBTypeConverter,Collection>() { + @Override + public List convert(final Collection o) { + return LIST.convert(o, target); + } + @Override + public Collection unconvert(final List o) { + return SET.unconvert(o, target); + } + }; + } + + Set unconvert(Collection o, DynamoDBTypeConverter scalar) { + Set vector = new LinkedHashSet(); + for (S s : o) { + if (vector.add(scalar.unconvert(s)) == false) { + throw new DynamoDBMappingException("duplicate value (" + s + ")"); + } + } + return vector; + } + + boolean is(final Class type) { + return Set.class.isAssignableFrom(type); + } + } + + /** + * Determines if the class represented by this vector is either the + * same as or a supertype of the specified target type. + */ + abstract boolean is(Class type); + } + + /** + * Converter map. + */ + private static class ConverterMap extends LinkedHashMap,Converter> { + private static final long serialVersionUID = -1L; + private final Class referenceType, primitiveType; + + private ConverterMap(Class referenceType, Class primitiveType) { + this.referenceType = referenceType; + this.primitiveType = primitiveType; + } + + private ConverterMap with(Class targetType, Converter converter) { + put(targetType, converter); + return this; + } + + private boolean isAssignableFrom(Class type) { + return type.isPrimitive() ? primitiveType == type : referenceType.isAssignableFrom(type); + } + + @SuppressWarnings("unchecked") + private Converter getConverter(Class targetType) { + for (Map.Entry,Converter> entry : entrySet()) { + if (entry.getKey().isAssignableFrom(targetType)) { + return (Converter)entry.getValue(); + } + } + if (isAssignableFrom(targetType)) { + return (Converter)ToObject.FromObject; + } + throw new DynamoDBMappingException( + "type [" + targetType + "] is not supported; no conversion from " + referenceType + ); + } + } + + /** + * {@link BigDecimal} conversion functions. + */ + private static abstract class ToBigDecimal extends Converter { + private static final ToBigDecimal FromString = new ToBigDecimal() { + @Override + public final BigDecimal convert(final String o) { + return new BigDecimal(o); + } + }; + } + + /** + * {@link BigInteger} conversion functions. + */ + private static abstract class ToBigInteger extends Converter { + private static final ToBigInteger FromString = new ToBigInteger() { + @Override + public final BigInteger convert(final String o) { + return new BigInteger(o); + } + }; + } + + /** + * {@link Boolean} conversion functions. + */ + private static abstract class ToBoolean extends Converter { + private static final ToBoolean FromString = new ToBoolean() { + private final Pattern N0 = Pattern.compile("(?i)[N0]"); + private final Pattern Y1 = Pattern.compile("(?i)[Y1]"); + @Override + public final Boolean convert(final String o) { + return N0.matcher(o).matches() ? Boolean.FALSE : Y1.matcher(o).matches() ? Boolean.TRUE : Boolean.valueOf(o); + } + }; + } + + /** + * {@link Byte} conversion functions. + */ + private static abstract class ToByte extends Converter { + private static final ToByte FromNumber = new ToByte() { + @Override + public final Byte convert(final Number o) { + return o.byteValue(); + } + }; + + private static final ToByte FromString = new ToByte() { + @Override + public final Byte convert(final String o) { + return Byte.valueOf(o); + } + }; + } + + /** + * {@link byte} array conversion functions. + */ + private static abstract class ToByteArray extends Converter { + private static final ToByteArray FromByteBuffer = new ToByteArray() { + @Override + public final byte[] convert(final ByteBuffer o) { + if (o.hasArray()) { + return o.array(); + } + byte[] value = new byte[o.remaining()]; + o.get(value); + return value; + } + }; + + private static final ToByteArray FromString = new ToByteArray() { + @Override + public final byte[] convert(final String o) { + return o.getBytes(Charset.forName("UTF-8")); + } + }; + } + + /** + * {@link ByteBuffer} conversion functions. + */ + private static abstract class ToByteBuffer extends Converter { + private static final ToByteBuffer FromByteArray = new ToByteBuffer() { + @Override + public final ByteBuffer convert(final byte[] o) { + return ByteBuffer.wrap(o); + } + }; + + private static final ToByteBuffer FromUuid = new ToByteBuffer() { + @Override + public final ByteBuffer convert(final java.util.UUID o) { + ByteBuffer value = ByteBuffer.allocate(16); + value.putLong(o.getMostSignificantBits()).putLong(o.getLeastSignificantBits()); + value.position(0); + return value; + } + }; + } + + /** + * {@link Calendar} conversion functions. + */ + private static abstract class ToCalendar extends Converter { + private static final ToCalendar FromDate = new ToCalendar() { + @Override + public final Calendar convert(final Date o) { + Calendar value = Calendar.getInstance(); + value.setTime(o); + return value; + } + }; + } + + /** + * {@link Character} conversion functions. + */ + private static abstract class ToCharacter extends Converter { + private static final ToCharacter FromString = new ToCharacter() { + @Override + public final Character convert(final String o) { + return Character.valueOf(o.charAt(0)); + } + }; + } + + /** + * {@link Currency} conversion functions. + */ + private static abstract class ToCurrency extends Converter { + private static final ToCurrency FromString = new ToCurrency() { + @Override + public final Currency convert(final String o) { + return Currency.getInstance(o); + } + }; + } + + /** + * {@link Date} conversion functions. + */ + private static abstract class ToDate extends Converter { + private static final ToDate FromCalendar = new ToDate() { + @Override + public final Date convert(final Calendar o) { + return o.getTime(); + } + }; + + private static final ToDate FromDateTime = new ToDate() { + @Override + public final Date convert(final DateTime o) { + return o.toDate(); + } + }; + + private static final ToDate FromLong = new ToDate() { + @Override + public final Date convert(final Long o) { + return new Date(o); + } + }; + + private static final ToDate FromString = new ToDate() { + @Override + public final Date convert(final String o) { + return MapperDateUtils.parseISO8601Date(o); + } + }; + } + + /** + * {@link DateTime} conversion functions. + */ + private static abstract class ToDateTime extends Converter { + private static final ToDateTime FromDate = new ToDateTime() { + public final DateTime convert(final Date o) { + return new DateTime(o); + } + }; + } + + /** + * {@link Double} conversion functions. + */ + private static abstract class ToDouble extends Converter { + private static final ToDouble FromNumber = new ToDouble() { + @Override + public final Double convert(final Number o) { + return o.doubleValue(); + } + }; + + private static final ToDouble FromString = new ToDouble() { + @Override + public final Double convert(final String o) { + return Double.valueOf(o); + } + }; + } + + /** + * {@link Enum} from {@link String} + */ + private static abstract class ToEnum,T> extends Converter { + private static final class FromString> extends ToEnum { + private final Class sourceType; + private FromString(final Class sourceType) { + this.sourceType = sourceType; + } + @Override + public final S convert(final String o) { + return Enum.valueOf(sourceType, o); + } + } + } + + /** + * {@link Float} conversion functions. + */ + private static abstract class ToFloat extends Converter { + private static final ToFloat FromNumber = new ToFloat() { + @Override + public final Float convert(final Number o) { + return o.floatValue(); + } + }; + + private static final ToFloat FromString = new ToFloat() { + @Override + public final Float convert(final String o) { + return Float.valueOf(o); + } + }; + } + + /** + * {@link Integer} conversion functions. + */ + private static abstract class ToInteger extends Converter { + private static final ToInteger FromNumber = new ToInteger() { + @Override + public final Integer convert(final Number o) { + return o.intValue(); + } + }; + + private static final ToInteger FromString = new ToInteger() { + @Override + public final Integer convert(final String o) { + return Integer.valueOf(o); + } + }; + } + + /** + * {@link Locale} conversion functions. + */ + private static abstract class ToLocale extends Converter { + private static final ToLocale FromString = new ToLocale() { + @Override + public final Locale convert(final String o) { + String[] value = o.split("-", 3); + if (value.length == 3) return new Locale(value[0], value[1], value[2]); + if (value.length == 2) return new Locale(value[0], value[1]); + return new Locale(value[0]); //JDK7+: return Locale.forLanguageTag(o); + } + }; + } + + /** + * {@link Long} conversion functions. + */ + private static abstract class ToLong extends Converter { + private static final ToLong FromDate = new ToLong() { + @Override + public final Long convert(final Date o) { + return o.getTime(); + } + }; + + private static final ToLong FromNumber = new ToLong() { + @Override + public final Long convert(final Number o) { + return o.longValue(); + } + }; + + private static final ToLong FromString = new ToLong() { + @Override + public final Long convert(final String o) { + return Long.valueOf(o); + } + }; + } + + /** + * {@link Short} conversion functions. + */ + private static abstract class ToShort extends Converter { + private static final ToShort FromNumber = new ToShort() { + @Override + public final Short convert(final Number o) { + return o.shortValue(); + } + }; + + private static final ToShort FromString = new ToShort() { + @Override + public final Short convert(final String o) { + return Short.valueOf(o); + } + }; + } + + /** + * {@link String} conversion functions. + */ + private static abstract class ToString extends Converter { + private static final ToString FromBoolean = new ToString() { + @Override + public final String convert(final Boolean o) { + return Boolean.TRUE.equals(o) ? "1" : "0"; + } + }; + + private static final ToString FromByteArray = new ToString() { + @Override + public final String convert(final byte[] o) { + return new String(o, Charset.forName("UTF-8")); + } + }; + + private static final ToString FromDate = new ToString() { + @Override + public final String convert(final Date o) { + return MapperDateUtils.formatISO8601Date(o); + } + }; + + private static final ToString FromEnum = new ToString() { + @Override + public final String convert(final Enum o) { + return o.name(); + } + }; + + private static final ToString FromLocale = new ToString() { + @Override + public final String convert(final Locale o) { + StringBuilder value = new StringBuilder(o.getLanguage()); + if (!o.getCountry().isEmpty() || !o.getVariant().isEmpty()) { + value.append("-").append(o.getCountry()); + } + if (!o.getVariant().isEmpty()) { + value.append("-").append(o.getVariant()); + } + return value.toString(); //JDK7+: return o.toLanguageTag(); + } + }; + + private static final ToString FromNumber = new ToString() { + @Override + public final String convert(final Number o) { + return o.toString(); + } + }; + + private static final ToString FromTimeZone = new ToString() { + @Override + public final String convert(final TimeZone o) { + return o.getID(); + } + }; + + private static final ToString FromObject = new ToString() { + @Override + public final String convert(final Object o) { + return o.toString(); + } + }; + } + + /** + * {@link TimeZone} conversion functions. + */ + private static abstract class ToTimeZone extends Converter { + private static final ToTimeZone FromString = new ToTimeZone() { + @Override + public final TimeZone convert(final String o) { + return TimeZone.getTimeZone(o); + } + }; + } + + /** + * {@link java.net.URL} conversion functions. + */ + private static abstract class ToUrl extends Converter { + private static final ToUrl FromString = new ToUrl() { + @Override + public final java.net.URL convert(final String o) { + try { + return new java.net.URL(o); + } catch (final java.net.MalformedURLException e) { + throw new IllegalArgumentException("malformed URL", e); + } + } + }; + } + + /** + * {@link java.net.URI} conversion functions. + */ + private static abstract class ToUri extends Converter { + private static final ToUri FromString = new ToUri() { + @Override + public final java.net.URI convert(final String o) { + try { + return new java.net.URI(o); + } catch (final java.net.URISyntaxException e) { + throw new IllegalArgumentException("malformed URI", e); + } + } + }; + } + + /** + * {@link java.util.UUID} conversion functions. + */ + private static abstract class ToUuid extends Converter { + private static final ToUuid FromByteBuffer = new ToUuid() { + @Override + public final java.util.UUID convert(final ByteBuffer o) { + return new java.util.UUID(o.getLong(), o.getLong()); + } + }; + + private static final ToUuid FromString = new ToUuid() { + @Override + public final java.util.UUID convert(final String o) { + return java.util.UUID.fromString(o); + } + }; + } + + /** + * {@link Object} conversion functions. + */ + private static abstract class ToObject extends Converter { + private static final ToObject FromObject = new ToObject() { + @Override + public final Object convert(final Object o) { + return o; + } + }; + } + + /** + * One-way type-converter. + */ + static abstract class Converter { + final Converter join(final Converter target) { + Converter source = this; + return new Converter() { + @Override + public S convert(final U o) { + return source.convert(target.convert(o)); + } + }; + } + public abstract S convert(T o); + } + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/TransactionLoadRequest.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/TransactionLoadRequest.java new file mode 100644 index 000000000000..8d31884baa81 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/TransactionLoadRequest.java @@ -0,0 +1,76 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import java.util.ArrayList; +import java.util.List; + +import software.amazon.awssdk.annotations.NotThreadSafe; + +/** + * Represents objects to load using {@link DynamoDBMapper#transactionLoad(TransactionLoadRequest)} operation. + */ +@SdkPublicApi +@NotThreadSafe +public final class TransactionLoadRequest { + /** + * Key objects, corresponding to the classes to load, with their primary key values set. + */ + private final List objectsToLoad; + + /** + * Projection expressions for loading specific item attributes. By default, all item attributes are loaded. + */ + private final List objectLoadExpressions; + + public TransactionLoadRequest() { + objectsToLoad = new ArrayList(); + objectLoadExpressions = new ArrayList(); + } + + /** + * Adds specified key to list of objects to load. All item attributes will be loaded. + */ + public TransactionLoadRequest addLoad(Object key) { + objectsToLoad.add(key); + objectLoadExpressions.add(null); + return this; + } + + /** + * Adds specified key to list of objects to load. Item attributes will be loaded as specified by transactLoadExpression. + */ + public TransactionLoadRequest addLoad(Object key, DynamoDBTransactionLoadExpression transactLoadExpression) { + objectsToLoad.add(key); + objectLoadExpressions.add(transactLoadExpression); + return this; + } + + /** + * Key objects corresponding to the classes to load. + */ + public List getObjectsToLoad() { + return objectsToLoad; + } + + /** + * Projection expressions for loading specific item attributes. + */ + public List getObjectLoadExpressions() { + return objectLoadExpressions; + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/TransactionWriteRequest.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/TransactionWriteRequest.java new file mode 100644 index 000000000000..4802f799e17d --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/TransactionWriteRequest.java @@ -0,0 +1,231 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import java.util.ArrayList; +import java.util.List; + +import software.amazon.awssdk.annotations.NotThreadSafe; +import software.amazon.awssdk.services.dynamodb.model.ReturnValuesOnConditionCheckFailure; + +/** + * Represents objects to write using {@link DynamoDBMapper#transactionWrite(TransactionWriteRequest)} operation. + */ +@SdkPublicApi +@NotThreadSafe +public final class TransactionWriteRequest { + + /** + * List of TransactionWriteOperations where each TransactionWriteOperation represents a write operation to be executed on an object as part of + * this transactional write request. + */ + private final List transactionWriteOperations; + + /** + * Token used to support idempotency on server side. For more information, please refer: + * https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactWriteItems.html + */ + private String idempotencyToken; + + public TransactionWriteRequest() { + transactionWriteOperations = new ArrayList(); + } + + /** + * Adds put operation (to be executed on object) to the list of transaction write operations. + * If the corresponding item exists, it will be overwritten. + */ + public TransactionWriteRequest addPut(Object object) { + return addPut(object, null /* transactionWriteExpression */); + } + + /** + * Adds put operation (to be executed on object) to the list of transaction write operations. + * transactionWriteExpression is used to conditionally put object. + */ + public TransactionWriteRequest addPut(Object object, DynamoDBTransactionWriteExpression transactionWriteExpression) { + return addPut(object, transactionWriteExpression, null /* returnValuesOnConditionCheckFailure */); + } + + /** + * Adds put operation (to be executed on object) to the list of transaction write operations. + * transactionWriteExpression is used to conditionally put object. + * returnValuesOnConditionCheckFailure specifies which attributes values (of existing item) should be returned if condition check fails. + */ + public TransactionWriteRequest addPut(Object object, + DynamoDBTransactionWriteExpression transactionWriteExpression, + ReturnValuesOnConditionCheckFailure returnValuesOnConditionCheckFailure) { + transactionWriteOperations.add(new TransactionWriteOperation(object, TransactionWriteOperationType.Put, transactionWriteExpression, returnValuesOnConditionCheckFailure)); + return this; + } + + /** + * Adds update operation (to be executed on object) to the list of transaction write operations. + */ + public TransactionWriteRequest addUpdate(Object object) { + return addUpdate(object, null /* transactionWriteExpression */); + } + + /** + * Adds update operation (to be executed on object) to the list of transaction write operations. + * transactionWriteExpression is used to conditionally update object. + */ + public TransactionWriteRequest addUpdate(Object object, DynamoDBTransactionWriteExpression transactionWriteExpression) { + return addUpdate(object, transactionWriteExpression, null /* returnValuesOnConditionCheckFailure */); + } + + /** + * Adds update operation (to be executed on object) to the list of transaction write operations. + * transactionWriteExpression is used to conditionally update object. + * returnValuesOnConditionCheckFailure specifies which attributes values (of existing item) should be returned if condition check fails. + */ + public TransactionWriteRequest addUpdate(Object object, + DynamoDBTransactionWriteExpression transactionWriteExpression, + ReturnValuesOnConditionCheckFailure returnValuesOnConditionCheckFailure) { + transactionWriteOperations.add(new TransactionWriteOperation(object, TransactionWriteOperationType.Update, transactionWriteExpression, returnValuesOnConditionCheckFailure)); + return this; + } + + /** + * Adds delete operation (to be executed on the object represented by key) to the list of transaction write operations. + */ + public TransactionWriteRequest addDelete(Object key) { + return addDelete(key, null /* transactionWriteExpression */); + } + + /** + * Adds delete operation (to be executed on the object represented by key) to the list of transaction write operations. + * transactionWriteExpression is used to conditionally delete the object represented by key. + */ + public TransactionWriteRequest addDelete(Object key, DynamoDBTransactionWriteExpression transactionWriteExpression) { + return addDelete(key, transactionWriteExpression, null /* returnValuesOnConditionCheckFailure */); + } + + /** + * Adds delete operation (to be executed on the object represented by key) to the list of transaction write operations. + * transactionWriteExpression is used to conditionally delete the object represented by key. + * returnValuesOnConditionCheckFailure specifies which attributes values (of existing item) should be returned if condition check fails. + */ + public TransactionWriteRequest addDelete(Object key, + DynamoDBTransactionWriteExpression transactionWriteExpression, + ReturnValuesOnConditionCheckFailure returnValuesOnConditionCheckFailure ) { + transactionWriteOperations.add(new TransactionWriteOperation(key, TransactionWriteOperationType.Delete, transactionWriteExpression, returnValuesOnConditionCheckFailure)); + return this; + } + + /** + * Adds conditionCheck operation (to be executed on the object represented by key) to the list of transaction write operations. + * transactionWriteExpression is used to condition check on the object represented by key. + */ + public TransactionWriteRequest addConditionCheck(Object key, DynamoDBTransactionWriteExpression transactionWriteExpression) { + return addConditionCheck(key, transactionWriteExpression, null /* returnValuesOnConditionCheckFailure */); + } + + /** + * Adds conditionCheck operation (to be executed on the object represented by key) to the list of transaction write operations. + * transactionWriteExpression is used to condition check on the object represented by key. + * returnValuesOnConditionCheckFailure specifies which attributes values (of existing item) should be returned if condition check fails. + */ + public TransactionWriteRequest addConditionCheck(Object key, + DynamoDBTransactionWriteExpression transactionWriteExpression, + ReturnValuesOnConditionCheckFailure returnValuesOnConditionCheckFailure) { + transactionWriteOperations.add(new TransactionWriteOperation(key, TransactionWriteOperationType.ConditionCheck, transactionWriteExpression, returnValuesOnConditionCheckFailure)); + return this; + } + + /** + * Token used to support idempotency on server side. For more information, please refer: + * https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactWriteItems.html + */ + public TransactionWriteRequest withIdempotencyToken(String idempotencyToken) { + this.idempotencyToken = idempotencyToken; + return this; + } + + /** + * List of TransactionWriteOperations to be executed as part of this transactional write request. + */ + public List getTransactionWriteOperations() { + return transactionWriteOperations; + } + + /** + * Token used to support idempotency on server side. + */ + public String getIdempotencyToken() { + return idempotencyToken; + } + + /** + * Represents a write operation to be executed on an object as part of a transactional write request. + */ + public static class TransactionWriteOperation { + private Object object; + private TransactionWriteOperationType transactionWriteOperationType; + private DynamoDBTransactionWriteExpression dynamoDBTransactionWriteExpression; + private ReturnValuesOnConditionCheckFailure returnValuesOnConditionCheckFailure; + + public TransactionWriteOperation(Object object, + TransactionWriteOperationType transactionWriteOperationType, + DynamoDBTransactionWriteExpression dynamoDBTransactionWriteExpression, + ReturnValuesOnConditionCheckFailure returnValuesOnConditionCheckFailure) { + this.object = object; + this.transactionWriteOperationType = transactionWriteOperationType; + this.dynamoDBTransactionWriteExpression = dynamoDBTransactionWriteExpression; + this.returnValuesOnConditionCheckFailure = returnValuesOnConditionCheckFailure; + } + + /** + * Object on which this operation would be executed. + */ + public Object getObject() { + return object; + } + + /** + * Type of this operation. + */ + public TransactionWriteOperationType getTransactionWriteOperationType() { + return transactionWriteOperationType; + } + + /** + * Write expression for conditionally executing this operation. + */ + public DynamoDBTransactionWriteExpression getDynamoDBTransactionWriteExpression() { + return dynamoDBTransactionWriteExpression; + } + + /** + * Return value type that specifies which attributes values (of existing item) should be returned if condition check fails. + */ + public ReturnValuesOnConditionCheckFailure getReturnValuesOnConditionCheckFailure() { + return returnValuesOnConditionCheckFailure; + } + + } + + /** + * Operation type for transactional write operations. + */ + public enum TransactionWriteOperationType { + Put, + Update, + ConditionCheck, + Delete + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/DynamoDBMapperModelFactory.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/DynamoDBMapperModelFactory.java new file mode 100644 index 000000000000..af4ad89fb19d --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/DynamoDBMapperModelFactory.java @@ -0,0 +1,44 @@ +/* + * 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.mapper.dynamodb.internal; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperTableModel; + +/** + * {@link DynamoDBMapper} table model factory. + */ +@SdkInternalApi +public interface DynamoDBMapperModelFactory { + + /** + * Gets/creates the mapper's model factory. + */ + public TableFactory getTableFactory(DynamoDBMapperConfig config); + + /** + * {@link DynamoDBMapperModelFactory} factory. + */ + public static interface TableFactory { + /** + * Gets the table model for the given type and configuration. + */ + public DynamoDBMapperTableModel getTable(Class clazz); + } + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/MapperBinaryUtils.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/MapperBinaryUtils.java new file mode 100644 index 000000000000..fd38e4660808 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/MapperBinaryUtils.java @@ -0,0 +1,43 @@ +/* + * 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.mapper.dynamodb.internal; + +import java.nio.ByteBuffer; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.core.SdkBytes; + +/** + * Binary conversion helpers shared by the mapper's converters and unmarshallers. + */ +@SdkInternalApi +public final class MapperBinaryUtils { + + private MapperBinaryUtils() { + } + + /** + * Returns a writable {@link ByteBuffer} containing the given bytes, preserving v1 behavior. + * {@link SdkBytes#asByteBuffer()} is read-only, so this wraps the copy from + * {@link SdkBytes#asByteArray()} instead. + * + * @param sdkBytes the bytes to wrap, may be {@code null} + * @return a writable buffer, or {@code null} if {@code sdkBytes} is {@code null} + */ + public static ByteBuffer toWritableByteBuffer(SdkBytes sdkBytes) { + return sdkBytes == null ? null : ByteBuffer.wrap(sdkBytes.asByteArray()); + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/MapperDateUtils.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/MapperDateUtils.java new file mode 100644 index 000000000000..ddfe972e6dc0 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/MapperDateUtils.java @@ -0,0 +1,132 @@ +/* + * 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.mapper.dynamodb.internal; + +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +import org.joda.time.DateTimeZone; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; +import org.joda.time.format.ISODateTimeFormat; +import org.joda.time.tz.FixedDateTimeZone; + +import software.amazon.awssdk.annotations.SdkInternalApi; + +/** + * A thin port of only the ISO-8601 parse/format methods of the v1 {@code com.amazonaws.util.DateUtils} + * that the mapper's converter layer uses, kept on the same Joda-Time formatters to preserve byte-for-byte + * compatibility with dates written by the v1 mapper. + */ +@SdkInternalApi +public final class MapperDateUtils { + + private static final DateTimeZone GMT = new FixedDateTimeZone("GMT", "GMT", 0, 0); + private static final long MILLI_SECONDS_OF_365_DAYS = 365L * 24 * 60 * 60 * 1000; + + /** ISO 8601 format. */ + private static final DateTimeFormatter ISO8601_DATE_FORMAT = + ISODateTimeFormat.dateTime().withZone(GMT); + + /** Alternate ISO 8601 format without fractional seconds. */ + private static final DateTimeFormatter ALTERNATE_ISO8601_DATE_FORMAT = + DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss'Z'").withZone(GMT); + + /** ISO 8601 format with a UTC offset. */ + private static final DateTimeFormatter ISO8601_DATE_FORMAT_WITH_OFFSET = + DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ssZZ"); + + private static final List ALTERNATE_ISO8601_FORMATTERS = Arrays.asList( + ALTERNATE_ISO8601_DATE_FORMAT, ISO8601_DATE_FORMAT_WITH_OFFSET); + + private MapperDateUtils() { + } + + /** + * Parses the specified date string as an ISO 8601 date and returns the {@link Date} object. + * + * @param dateStringOrig the date string to parse + * @return the parsed {@link Date} object + */ + public static Date parseISO8601Date(final String dateStringOrig) { + String dateString = dateStringOrig; + + // For EC2 Spot Fleet. + if (dateString.endsWith("+0000")) { + dateString = dateString + .substring(0, dateString.length() - 5) + .concat("Z"); + } + + // https://github.com/aws/aws-sdk-java/issues/233 + String temp = tempDateStringForJodaTime(dateString); + try { + if (temp.equals(dateString)) { + // Normal case: nothing special here + return new Date(ISO8601_DATE_FORMAT.parseMillis(dateString)); + } + // Handling edge case: + // Joda-time can only handle up to year 292278993 but we are given + // 292278994; So we parse the date string by first adjusting + // the year to 292278993. Then we add 1 year back afterwards. + long milliLess365Days = ISO8601_DATE_FORMAT.parseMillis(temp); + long milli = milliLess365Days + MILLI_SECONDS_OF_365_DAYS; + if (milli < 0) { // overflow! + // re-parse the original date string using JodaTime so as to + // throw an exception with a consistent message + return new Date(ISO8601_DATE_FORMAT.parseMillis(dateString)); + } + return new Date(milli); + } catch (IllegalArgumentException e) { + for (DateTimeFormatter dateTimeFormatter : ALTERNATE_ISO8601_FORMATTERS) { + try { + // If the first ISO 8601 parser didn't work, try the alternate + // version which doesn't include fractional seconds + return new Date(dateTimeFormatter.parseMillis(dateString)); + } catch (Exception oops) { + // ignore + } + } + + throw e; + } + } + + /** + * Formats the specified date as an ISO 8601 string. + * + * @param date the date to format + * @return the ISO 8601 string representing the specified date + */ + public static String formatISO8601Date(final Date date) { + return ISO8601_DATE_FORMAT.print(date.getTime()); + } + + /** + * Returns a date string with the prefix temporarily substituted, if applicable, so that + * JodaTime can handle it. Otherwise, if not applicable, the original date string is returned. + * + *

See https://github.com/aws/aws-sdk-java/issues/233

+ */ + private static String tempDateStringForJodaTime(final String dateString) { + String fromPrefix = "292278994-"; + String toPrefix = "292278993-"; + return dateString.startsWith(fromPrefix) + ? toPrefix + dateString.substring(fromPrefix.length()) + : dateString; + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/UpdateExpressionGenerator.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/UpdateExpressionGenerator.java new file mode 100644 index 000000000000..8e41cef99262 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/UpdateExpressionGenerator.java @@ -0,0 +1,199 @@ +/* + * 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.mapper.dynamodb.internal; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.zip.Adler32; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMappingException; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * This class will contain logic for converting an update object that customer has passed-in into + * low level update expression and corresponding expression attribute maps. + * Currently this entails generating a string that can be used as a substitution token in the + * updateExpression and insert corresponding attribute name/value in the expressionAttributeNameMap/expressionAttributeValueMap + * + * For ex: Attribute will be converted to: + * updateExpression = "SET #5d810=:5d810" and + * expressionAttributeNameMap = {{#5d810, "stringAttribute"},...} + * expressionAttributeValueMap = {{:5d810, {S: "sdkAttributeStringValue"}},...} + * + * Null valued attributes will be converted to REMOVE expressions similarly and appended to the updateExpression + * + * We use deterministic token generation logic to ensure request contents remain same across retried transactionWrite calls. + * This will help in ensuring idempotency across retried calls. + * Read more on idempotency of transactionWrite API here: + * https://docs.aws.amazon.com/en_us/amazondynamodb/latest/APIReference/API_TransactWriteItems.html#DDB-TransactWriteItems-request-ClientRequestToken + */ +@SdkInternalApi +public class UpdateExpressionGenerator { + + /** + * Generate a string that can be used as a substitution token for attributeNames and attributeValues in + * update expression. De-dupes generated string on existingAttributesKeyList. + * The string starts with a baseToken's substring that is passed in from the caller and then appended with a hex string + * the hex string represents the locally incremented counter across attributes. + * In case there is a conflict with above generated string, we'll instead substitute with the full baseToken + * instead of the baseToken substring + * + * For ex: + * For a baseToken: 5d81, suffixCounter: 9 + * the *next* generated string will be 5d81a, 5d81 from baseToken and `a` from the incremented hex counter 9 -> a + */ + private String generateSubstitutionTokenString(String baseToken, + Long suffixCounter, + Set expressionAttributeNamesKeys, + Set expressionAttributeValuesKeys) { + String hexSuffix = Long.toHexString(suffixCounter); + String tokenBase = baseToken.substring(0, Math.min(baseToken.length(), 4)) + hexSuffix; + if (!expressionAttributeNamesKeys.contains(getExpressionAttributeNameSubstitutionToken(tokenBase)) && + !expressionAttributeValuesKeys.contains(getExpressionAttributeValueSubstitutionToken(tokenBase))) { + return tokenBase; + } + // Fallback to fullToken in case of conflict + String fullToken = baseToken + hexSuffix; + if (!expressionAttributeNamesKeys.contains(getExpressionAttributeNameSubstitutionToken(fullToken)) && + !expressionAttributeValuesKeys.contains(getExpressionAttributeValueSubstitutionToken(fullToken))) { + return fullToken; + } else { + throw new DynamoDBMappingException("Failed to process update operation inside transactionWrite request due to conflict with expressionAttributeName or expressionAttributeValue token name: " + + fullToken + ". Please replace this token name with a different token name."); + } + } + + private String generateSubstitutionTokenString(String baseToken, + Long suffixCounter, + Set expressionAttributeNamesKeys) { + return generateSubstitutionTokenString(baseToken, + suffixCounter, + expressionAttributeNamesKeys, + Collections.emptySet()); + } + + /** + * Merge nonKeyAttributes along with any condition expression attributes and generate corresponding update expression + */ + public String generateUpdateExpressionAndUpdateAttributeMaps(Map expressionAttributeNamesMap, + Map expressionsAttributeValuesMap, + Map nonKeyNonNullAttributeValueMap, + List nullValuedNonKeyAttributeNames) { + StringBuilder updateExpressionSetBuilder = new StringBuilder(); + StringBuilder updateExpressionDeleteBuilder = new StringBuilder(); + + // This has to be a sorted list to ensure deterministic retrieval order for nonKeyNonNullAttributes + // while auto generating update expression tokens + List sortedNonKeyNonNullAttributeNames = new ArrayList(nonKeyNonNullAttributeValueMap.keySet()); + Collections.sort(sortedNonKeyNonNullAttributeNames); + + // Sort the null valued non-key attributes to ensure deterministic retrieval order for nullValuedNonKeyAttributes + // while auto generating update expression tokens + List sortedNullValuedNonKeyAttributeNames = new ArrayList(nullValuedNonKeyAttributeNames); + Collections.sort(sortedNullValuedNonKeyAttributeNames); + + + // Initialize baseToken and suffixCounter for each update item + String baseToken = getBaseToken(sortedNonKeyNonNullAttributeNames, sortedNullValuedNonKeyAttributeNames); + Long suffixCounter = 0L; + if (sortedNonKeyNonNullAttributeNames.size() > 0) { + updateExpressionSetBuilder.append("SET "); + List updateStringSetExpressions = new ArrayList(); + for (String nonKeyAttributeName : sortedNonKeyNonNullAttributeNames) { + String tokenBase = generateSubstitutionTokenString(baseToken, + suffixCounter, + expressionAttributeNamesMap.keySet(), + expressionsAttributeValuesMap.keySet()); + suffixCounter++; + String tokenKeyName = getExpressionAttributeNameSubstitutionToken(tokenBase); + String tokenValueName = getExpressionAttributeValueSubstitutionToken(tokenBase); + expressionAttributeNamesMap.put(tokenKeyName, nonKeyAttributeName); + expressionsAttributeValuesMap.put(tokenValueName, nonKeyNonNullAttributeValueMap.get(nonKeyAttributeName)); + updateStringSetExpressions.add(tokenKeyName + " = " + tokenValueName); + } + for (int i = 0; i < updateStringSetExpressions.size() - 1; i++) { + updateExpressionSetBuilder.append(updateStringSetExpressions.get(i) + ", "); + } + updateExpressionSetBuilder.append(updateStringSetExpressions.get(updateStringSetExpressions.size() - 1)); + + } + if (sortedNullValuedNonKeyAttributeNames.size() > 0) { + updateExpressionDeleteBuilder.append("REMOVE "); + List updateStringDeleteExpressions = new ArrayList(); + for (String nullAttributeName : sortedNullValuedNonKeyAttributeNames) { + String tokenBaseString = generateSubstitutionTokenString(baseToken, + suffixCounter, + expressionAttributeNamesMap.keySet()); + suffixCounter++; + String tokenKeyName = getExpressionAttributeNameSubstitutionToken(tokenBaseString); + expressionAttributeNamesMap.put(tokenKeyName, nullAttributeName); + updateStringDeleteExpressions.add(tokenKeyName); + } + for (int i = 0; i < updateStringDeleteExpressions.size() - 1; i++) { + updateExpressionDeleteBuilder.append(updateStringDeleteExpressions.get(i) + ", "); + } + updateExpressionDeleteBuilder.append(updateStringDeleteExpressions.get(updateStringDeleteExpressions.size() - 1)); + } + StringBuilder updateExpression = new StringBuilder(); + if (updateExpressionSetBuilder.length() > 0) { + updateExpression.append(updateExpressionSetBuilder.toString()); + } + if (updateExpressionDeleteBuilder.length() > 0) { + updateExpression.append(" " + updateExpressionDeleteBuilder.toString()); + } + + return updateExpression.toString(); + } + + private static String getExpressionAttributeNameSubstitutionToken(String tokenBase) { + return "#" + tokenBase; + } + + private static String getExpressionAttributeValueSubstitutionToken(String tokenBase) { + return ":" + tokenBase; + } + + /** + * Uses attribute names from sortedNonKeyNonNullAttributeNames and sortedNullValuedNonKeyAttributeNames to generate an {@link Adler32} checkSum + * Adler32 generates checksum that is unique enough to be used as a token within an update item and faster to compute than CRC32. + */ + private String getBaseToken(List sortedNonKeyNonNullAttributeNames, + List sortedNullValuedNonKeyAttributeNames) { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + DataOutputStream dataOutputStream = new DataOutputStream(byteArrayOutputStream); + try { + for (String nonKeyNonNullAttributeName : sortedNonKeyNonNullAttributeNames) { + dataOutputStream.writeUTF(nonKeyNonNullAttributeName); + } + for (String nullValuedNonKeyAttributeName : sortedNullValuedNonKeyAttributeNames) { + dataOutputStream.writeUTF(nullValuedNonKeyAttributeName); + } + } catch (IOException e) { + throw new DynamoDBMappingException("Failed to process update operation inside transactionWrite request due to an IOException ", e); + } + Adler32 adler32 = new Adler32(); + adler32.update(byteArrayOutputStream.toByteArray()); + return Long.toHexString(adler32.getValue()); + } + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/VersionAttributeConditionExpressionGenerator.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/VersionAttributeConditionExpressionGenerator.java new file mode 100644 index 000000000000..ac79ebeb919d --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/VersionAttributeConditionExpressionGenerator.java @@ -0,0 +1,105 @@ +/* + * 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.mapper.dynamodb.internal; + +import java.util.HashMap; +import java.util.Map; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperFieldModel; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTransactionWriteExpression; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * This class contains logic to generate condition expressions for version attribute + * TODO: Change the logic in this class to use Builder pattern instead. This will help avoid state leak that this class + * currently has. + */ +@SdkInternalApi +public class VersionAttributeConditionExpressionGenerator { + private static final String VERSION_ATTRIBUTE_NAME_BASE_TOKEN = "#versionAttributeName"; + private static final String VERSION_ATTRIBUTE_VALUE_BASE_TOKEN = ":versionAttributeValue"; + private final DynamoDBTransactionWriteExpression versionAttributeConditionExpression + = new DynamoDBTransactionWriteExpression(); + private Long versionAttributeCounterValue = 0L; + + /** + * Generates condition expression for a given version attribute and appends this generated condition expression to + * any previously generated version attribute condition expressions. + */ + public void appendVersionAttributeToConditionExpression( + DynamoDBMapperFieldModel field, + Object fieldValue) { + versionAttributeCounterValue += 1L; + String versionAttributeNameToken = VERSION_ATTRIBUTE_NAME_BASE_TOKEN + versionAttributeCounterValue; + String versionAttributeValueToken = VERSION_ATTRIBUTE_VALUE_BASE_TOKEN + versionAttributeCounterValue; + Map currentVersionedAttributeConditionExpressionNames = new HashMap(); + currentVersionedAttributeConditionExpressionNames.put(versionAttributeNameToken, field.name()); + Map currentVersionedAttributeConditionExpressionValues = null; + String currentVersionConditionExpression = null; + if (fieldValue == null) { + currentVersionConditionExpression = String.format("attribute_not_exists(%s)", versionAttributeNameToken); + } else { + currentVersionedAttributeConditionExpressionValues = new HashMap(); + currentVersionedAttributeConditionExpressionValues.put(versionAttributeValueToken, + field.convert(fieldValue)); + currentVersionConditionExpression = String.format("attribute_exists(%s) AND %s = %s", + versionAttributeNameToken, + versionAttributeNameToken, + versionAttributeValueToken); + } + + if (versionAttributeConditionExpression.getConditionExpression() != null) { + String currentConditionExpression = versionAttributeConditionExpression.getConditionExpression(); + if (versionAttributeCounterValue == 2) { + currentConditionExpression = "(" + currentConditionExpression + ")"; + } + versionAttributeConditionExpression.withConditionExpression( + currentConditionExpression + " AND (" + currentVersionConditionExpression + ")"); + + } else { + versionAttributeConditionExpression.withConditionExpression(currentVersionConditionExpression); + } + + if (versionAttributeConditionExpression.getExpressionAttributeNames() != null) { + versionAttributeConditionExpression.getExpressionAttributeNames() + .putAll(currentVersionedAttributeConditionExpressionNames); + } else { + versionAttributeConditionExpression.withExpressionAttributeNames(currentVersionedAttributeConditionExpressionNames); + } + + if (currentVersionedAttributeConditionExpressionValues != null) { + if (versionAttributeConditionExpression.getExpressionAttributeValues() != null) { + versionAttributeConditionExpression.getExpressionAttributeValues() + .putAll(currentVersionedAttributeConditionExpressionValues); + } else { + versionAttributeConditionExpression.withExpressionAttributeValues( + currentVersionedAttributeConditionExpressionValues); + } + } + } + + /** + * Call this method *after* you've completed calling appendVersionAttributeToConditionExpression() for each of the + * version attributes in a given item. + * + * The versionAttributeConditionExpression being returned is mutable inside and outside of this class. + */ + public DynamoDBTransactionWriteExpression getVersionAttributeConditionExpression() { + return versionAttributeConditionExpression; + } +} + diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/BooleanSetToNumberSetMarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/BooleanSetToNumberSetMarshaller.java new file mode 100644 index 000000000000..4ca5346fafc3 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/BooleanSetToNumberSetMarshaller.java @@ -0,0 +1,62 @@ +/* + * 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.mapper.dynamodb.internal.marshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import software.amazon.awssdk.mapper.dynamodb.ArgumentMarshaller.NumberSetAttributeMarshaller; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * A legacy marshaller that marshals sets of Java {@code Booleans} into DynamoDB + * NumberSets, representing {@code true} as '1' and {@code false} as '0'. + * Retained for backwards compatibility with older versions of the mapper which + * don't know about the DynamoDB BOOL type. + */ +@SdkInternalApi +public class BooleanSetToNumberSetMarshaller + implements NumberSetAttributeMarshaller { + + private static final BooleanSetToNumberSetMarshaller INSTANCE = + new BooleanSetToNumberSetMarshaller(); + + public static BooleanSetToNumberSetMarshaller instance() { + return INSTANCE; + } + + private BooleanSetToNumberSetMarshaller() { + } + + @Override + public AttributeValue marshall(Object obj) { + @SuppressWarnings("unchecked") + Set booleans = (Set) obj; + List booleanAttributes = new ArrayList(booleans.size()); + + for (Boolean b : booleans) { + if (b == null || b == false) { + booleanAttributes.add("0"); + } else { + booleanAttributes.add("1"); + } + } + + return AttributeValue.builder().ns(booleanAttributes).build(); + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/BooleanToBooleanMarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/BooleanToBooleanMarshaller.java new file mode 100644 index 000000000000..d27048992348 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/BooleanToBooleanMarshaller.java @@ -0,0 +1,43 @@ +/* + * 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.mapper.dynamodb.internal.marshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.mapper.dynamodb.ArgumentMarshaller.BooleanAttributeMarshaller; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * A marshaller that marshals Java {@code Boolean} objects to Dynamodb-native + * {@code BOOL} attribute values. + */ +@SdkInternalApi +public class BooleanToBooleanMarshaller implements BooleanAttributeMarshaller { + + private static final BooleanToBooleanMarshaller INSTANCE = + new BooleanToBooleanMarshaller(); + + public static BooleanToBooleanMarshaller instance() { + return INSTANCE; + } + + private BooleanToBooleanMarshaller() { + } + + @Override + public AttributeValue marshall(Object obj) { + return AttributeValue.builder().bool((Boolean) obj).build(); + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/BooleanToNumberMarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/BooleanToNumberMarshaller.java new file mode 100644 index 000000000000..ea3a8d2fa1dc --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/BooleanToNumberMarshaller.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.mapper.dynamodb.internal.marshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.mapper.dynamodb.ArgumentMarshaller.NumberAttributeMarshaller; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * A legacy marshaller that marshals Java {@code Booleans} into DynamoDB + * Numbers, representing {@code true} as '1' and {@code false} as '0'. Retained + * for backwards compatibility with older versions of the mapper which don't + * know about the DynamoDB BOOL type. + */ +@SdkInternalApi +public class BooleanToNumberMarshaller implements NumberAttributeMarshaller { + + private static final BooleanToNumberMarshaller INSTANCE = + new BooleanToNumberMarshaller(); + + public static BooleanToNumberMarshaller instance() { + return INSTANCE; + } + + private BooleanToNumberMarshaller() { + } + + @Override + public AttributeValue marshall(Object obj) { + Boolean bool = (Boolean) obj; + if (bool == null || bool == false) { + return AttributeValue.builder().n("0").build(); + } else { + return AttributeValue.builder().n("1").build(); + } + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/ByteArraySetToBinarySetMarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/ByteArraySetToBinarySetMarshaller.java new file mode 100644 index 000000000000..e9b0fa5623d9 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/ByteArraySetToBinarySetMarshaller.java @@ -0,0 +1,58 @@ +/* + * 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.mapper.dynamodb.internal.marshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import software.amazon.awssdk.mapper.dynamodb.ArgumentMarshaller.BinarySetAttributeMarshaller; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * A marshaller that marshals sets of Java {@code byte[]}s into DynamoDB + * BinarySet attributes. + */ +@SdkInternalApi +public class ByteArraySetToBinarySetMarshaller + implements BinarySetAttributeMarshaller { + + private static final ByteArraySetToBinarySetMarshaller INSTANCE = + new ByteArraySetToBinarySetMarshaller(); + + public static ByteArraySetToBinarySetMarshaller instance() { + return INSTANCE; + } + + private ByteArraySetToBinarySetMarshaller() { + } + + @Override + public AttributeValue marshall(Object obj) { + @SuppressWarnings("unchecked") + Set buffers = (Set) obj; + List attributes = new ArrayList(buffers.size()); + + for (byte[] b : buffers) { + attributes.add(SdkBytes.fromByteArray(b)); + } + + return AttributeValue.builder().bs(attributes).build(); + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/ByteArrayToBinaryMarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/ByteArrayToBinaryMarshaller.java new file mode 100644 index 000000000000..b05c3742c574 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/ByteArrayToBinaryMarshaller.java @@ -0,0 +1,46 @@ +/* + * 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.mapper.dynamodb.internal.marshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import java.nio.ByteBuffer; + +import software.amazon.awssdk.mapper.dynamodb.ArgumentMarshaller.BinaryAttributeMarshaller; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * A marshaller that marshals Java {@code byte[]}s into DynamoDB Binary + * attributes. + */ +@SdkInternalApi +public class ByteArrayToBinaryMarshaller implements BinaryAttributeMarshaller { + + private static final ByteArrayToBinaryMarshaller INSTANCE = + new ByteArrayToBinaryMarshaller(); + + public static ByteArrayToBinaryMarshaller instance() { + return INSTANCE; + } + + private ByteArrayToBinaryMarshaller() { + } + + @Override + public AttributeValue marshall(Object obj) { + return AttributeValue.builder().b(SdkBytes.fromByteArray((byte[]) obj)).build(); + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/ByteBufferSetToBinarySetMarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/ByteBufferSetToBinarySetMarshaller.java new file mode 100644 index 000000000000..3bf1f883c509 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/ByteBufferSetToBinarySetMarshaller.java @@ -0,0 +1,58 @@ +/* + * 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.mapper.dynamodb.internal.marshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import software.amazon.awssdk.mapper.dynamodb.ArgumentMarshaller.BinarySetAttributeMarshaller; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * A marshaller that marshals sets of Java {@code ByteBuffer}s into DynamoDB + * BinarySet attributes. + */ +@SdkInternalApi +public class ByteBufferSetToBinarySetMarshaller + implements BinarySetAttributeMarshaller { + + private static final ByteBufferSetToBinarySetMarshaller INSTANCE = + new ByteBufferSetToBinarySetMarshaller(); + + public static ByteBufferSetToBinarySetMarshaller instance() { + return INSTANCE; + } + + private ByteBufferSetToBinarySetMarshaller() { + } + + @Override + public AttributeValue marshall(Object obj) { + @SuppressWarnings("unchecked") + Set buffers = (Set) obj; + List attributes = new ArrayList(buffers.size()); + + for (ByteBuffer b : buffers) { + attributes.add(SdkBytes.fromByteBuffer(b)); + } + + return AttributeValue.builder().bs(attributes).build(); + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/ByteBufferToBinaryMarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/ByteBufferToBinaryMarshaller.java new file mode 100644 index 000000000000..7b12fd6d2b72 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/ByteBufferToBinaryMarshaller.java @@ -0,0 +1,46 @@ +/* + * 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.mapper.dynamodb.internal.marshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import java.nio.ByteBuffer; + +import software.amazon.awssdk.mapper.dynamodb.ArgumentMarshaller.BinaryAttributeMarshaller; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * A marshaller that marshals Java {@code ByteBuffer}s into DynamoDB Binary + * attributes. + */ +@SdkInternalApi +public class ByteBufferToBinaryMarshaller implements BinaryAttributeMarshaller { + + private static final ByteBufferToBinaryMarshaller INSTANCE = + new ByteBufferToBinaryMarshaller(); + + public static ByteBufferToBinaryMarshaller instance() { + return INSTANCE; + } + + private ByteBufferToBinaryMarshaller() { + } + + @Override + public AttributeValue marshall(Object obj) { + return AttributeValue.builder().b(SdkBytes.fromByteBuffer((ByteBuffer) obj)).build(); + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/CalendarSetToStringSetMarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/CalendarSetToStringSetMarshaller.java new file mode 100644 index 000000000000..ae72f6518508 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/CalendarSetToStringSetMarshaller.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.mapper.dynamodb.internal.marshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.mapper.dynamodb.internal.MapperDateUtils; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; +import java.util.Set; + +import software.amazon.awssdk.mapper.dynamodb.ArgumentMarshaller.StringSetAttributeMarshaller; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * A marshaller that marshals sets of Java {@code Calendar} objects into + * DynamoDB StringSets (in ISO 8601 format, ie {"2014-01-01T00:00:00Z", ...}). + */ +@SdkInternalApi +public class CalendarSetToStringSetMarshaller + implements StringSetAttributeMarshaller { + + private static final CalendarSetToStringSetMarshaller INSTANCE = + new CalendarSetToStringSetMarshaller(); + + public static CalendarSetToStringSetMarshaller instance() { + return INSTANCE; + } + + private CalendarSetToStringSetMarshaller() { + } + + @Override + public AttributeValue marshall(Object obj) { + @SuppressWarnings("unchecked") + Set dates = (Set) obj; + + List timestamps = new ArrayList(dates.size()); + for (Calendar date : dates) { + timestamps.add(MapperDateUtils.formatISO8601Date(date.getTime())); + } + + return AttributeValue.builder().ss(timestamps).build(); + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/CalendarToStringMarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/CalendarToStringMarshaller.java new file mode 100644 index 000000000000..f7d532a01265 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/CalendarToStringMarshaller.java @@ -0,0 +1,48 @@ +/* + * 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.mapper.dynamodb.internal.marshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.mapper.dynamodb.internal.MapperDateUtils; + +import java.util.Calendar; + +import software.amazon.awssdk.mapper.dynamodb.ArgumentMarshaller.StringAttributeMarshaller; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * A marshaller that marshals Java {@code Calendar} objects into DynamoDB + * Strings (in ISO 8601 format, ie "2014-01-01T00:00:00Z"). + */ +@SdkInternalApi +public class CalendarToStringMarshaller implements StringAttributeMarshaller { + + private static final CalendarToStringMarshaller INSTANCE = + new CalendarToStringMarshaller(); + + public static CalendarToStringMarshaller instance() { + return INSTANCE; + } + + private CalendarToStringMarshaller() { + } + + @Override + public AttributeValue marshall(Object obj) { + return AttributeValue.builder().s( + MapperDateUtils.formatISO8601Date(((Calendar) obj).getTime())).build(); + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/CollectionToListMarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/CollectionToListMarshaller.java new file mode 100644 index 000000000000..dfa3addd94c0 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/CollectionToListMarshaller.java @@ -0,0 +1,76 @@ +/* + * 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.mapper.dynamodb.internal.marshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import software.amazon.awssdk.mapper.dynamodb.ArgumentMarshaller; +import software.amazon.awssdk.mapper.dynamodb.ArgumentMarshaller.ListAttributeMarshaller; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +@SdkInternalApi +public class CollectionToListMarshaller implements ListAttributeMarshaller { + + private static final CollectionToListMarshaller INSTANCE = + new CollectionToListMarshaller(); + + public static CollectionToListMarshaller instance() { + return INSTANCE; + } + + + private final ArgumentMarshaller memberMarshaller; + + private CollectionToListMarshaller() { + this(null); + } + + public CollectionToListMarshaller(ArgumentMarshaller memberMarshaller) { + this.memberMarshaller = memberMarshaller; + } + + @Override + public AttributeValue marshall(Object obj) { + if (memberMarshaller == null) { + throw new IllegalStateException( + "No member marshaller configured!"); + } + + Collection objects = (Collection) obj; + List values = + new ArrayList(objects.size()); + + for (Object o : objects) { + AttributeValue value; + if (o == null) { + value = AttributeValue.builder().nul(true).build(); + } else { + value = memberMarshaller.marshall(o); + } + + values.add(value); + } + + return AttributeValue.builder().l(values).build(); + } + + public ArgumentMarshaller getMemberMarshaller() { + return memberMarshaller; + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/CustomMarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/CustomMarshaller.java new file mode 100644 index 000000000000..55f8ab4b3bac --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/CustomMarshaller.java @@ -0,0 +1,75 @@ +/* + * 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.mapper.dynamodb.internal.marshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.mapper.dynamodb.ArgumentMarshaller.StringAttributeMarshaller; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMappingException; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMarshaller; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * A marshaller that delegates to an instance of a + * {@code DynamoDBMarshalling}-derived custom marshaler. + */ +@SdkInternalApi +public class CustomMarshaller implements StringAttributeMarshaller { + + private final Class> marshallerClass; + + public CustomMarshaller( + Class> marshallerClass) { + + this.marshallerClass = marshallerClass; + } + + @Override + public AttributeValue marshall(Object obj) { + + // TODO: Would be nice to cache this object, but not sure if we can + // do that now without a breaking change; user's marshalers might + // not all be thread-safe. + + DynamoDBMarshaller marshaler = + createMarshaller(marshallerClass); + + String stringValue = marshaler.marshall(obj); + + if (stringValue == null) { + return null; + } else { + return AttributeValue.builder().s(stringValue).build(); + } + } + + @SuppressWarnings("unchecked") + private static DynamoDBMarshaller createMarshaller(Class clazz) { + try { + + return (DynamoDBMarshaller) clazz.newInstance(); + + } catch (InstantiationException e) { + throw new DynamoDBMappingException( + "Failed to instantiate custom marshaler for class " + clazz, + e); + + } catch (IllegalAccessException e) { + throw new DynamoDBMappingException( + "Failed to instantiate custom marshaler for class " + clazz, + e); + } + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/DateSetToStringSetMarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/DateSetToStringSetMarshaller.java new file mode 100644 index 000000000000..ff2af8a2c9cc --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/DateSetToStringSetMarshaller.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.mapper.dynamodb.internal.marshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.mapper.dynamodb.internal.MapperDateUtils; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Set; + +import software.amazon.awssdk.mapper.dynamodb.ArgumentMarshaller.StringSetAttributeMarshaller; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * A marshaller that marshals sets of Java {@code Date} objects into DynamoDB + * StringSets (in ISO 8601 format, ie {"2014-01-01T00:00:00Z", ...}). + */ +@SdkInternalApi +public class DateSetToStringSetMarshaller + implements StringSetAttributeMarshaller { + + private static final DateSetToStringSetMarshaller INSTANCE = + new DateSetToStringSetMarshaller(); + + public static DateSetToStringSetMarshaller instance() { + return INSTANCE; + } + + private DateSetToStringSetMarshaller() { + } + + @Override + public AttributeValue marshall(Object obj) { + @SuppressWarnings("unchecked") + Set dates = (Set) obj; + + List timestamps = new ArrayList(dates.size()); + for (Date date : dates) { + timestamps.add(MapperDateUtils.formatISO8601Date(date)); + } + + return AttributeValue.builder().ss(timestamps).build(); + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/DateToStringMarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/DateToStringMarshaller.java new file mode 100644 index 000000000000..3f05bcc532fb --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/DateToStringMarshaller.java @@ -0,0 +1,48 @@ +/* + * 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.mapper.dynamodb.internal.marshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.mapper.dynamodb.internal.MapperDateUtils; + +import java.util.Date; + +import software.amazon.awssdk.mapper.dynamodb.ArgumentMarshaller.StringAttributeMarshaller; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * A marshaller that marshals Java {@code Date} objects into DynamoDB Strings + * (in ISO 8601 format, ie "2014-01-01T00:00:00Z"). + */ +@SdkInternalApi +public class DateToStringMarshaller implements StringAttributeMarshaller { + + private static final DateToStringMarshaller INSTANCE = + new DateToStringMarshaller(); + + public static DateToStringMarshaller instance() { + return INSTANCE; + } + + private DateToStringMarshaller() { + } + + @Override + public AttributeValue marshall(Object obj) { + return AttributeValue.builder().s( + MapperDateUtils.formatISO8601Date((Date) obj)).build(); + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/MapToMapMarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/MapToMapMarshaller.java new file mode 100644 index 000000000000..2dc995b7de45 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/MapToMapMarshaller.java @@ -0,0 +1,79 @@ +/* + * 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.mapper.dynamodb.internal.marshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import java.util.HashMap; +import java.util.Map; + +import software.amazon.awssdk.mapper.dynamodb.ArgumentMarshaller; +import software.amazon.awssdk.mapper.dynamodb.ArgumentMarshaller.MapAttributeMarshaller; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +@SdkInternalApi +public class MapToMapMarshaller implements MapAttributeMarshaller { + + private static final MapToMapMarshaller INSTANCE = + new MapToMapMarshaller(); + + public static MapToMapMarshaller instance() { + return INSTANCE; + } + + + private final ArgumentMarshaller memberMarshaller; + + private MapToMapMarshaller() { + memberMarshaller = null; + } + + public MapToMapMarshaller(ArgumentMarshaller memberMarshaller) { + if (memberMarshaller == null) { + throw new NullPointerException("memberMarshaller"); + } + this.memberMarshaller = memberMarshaller; + } + + @Override + public AttributeValue marshall(Object obj) { + if (memberMarshaller == null) { + throw new IllegalStateException( + "No member marshaller configured!"); + } + + @SuppressWarnings("unchecked") + Map map = (Map) obj; + Map values = + new HashMap(); + + for (Map.Entry entry : map.entrySet()) { + AttributeValue value; + if (entry.getValue() == null) { + value = AttributeValue.builder().nul(true).build(); + } else { + value = memberMarshaller.marshall(entry.getValue()); + } + + values.put(entry.getKey(), value); + } + + return AttributeValue.builder().m(values).build(); + } + + public ArgumentMarshaller getMemberMarshaller() { + return memberMarshaller; + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/NumberSetToNumberSetMarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/NumberSetToNumberSetMarshaller.java new file mode 100644 index 000000000000..926a17f7fe53 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/NumberSetToNumberSetMarshaller.java @@ -0,0 +1,56 @@ +/* + * 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.mapper.dynamodb.internal.marshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import software.amazon.awssdk.mapper.dynamodb.ArgumentMarshaller.NumberSetAttributeMarshaller; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * A marshaller that marshals sets of Java {@code Number}s into DynamoDB + * NumberSets. + */ +@SdkInternalApi +public class NumberSetToNumberSetMarshaller + implements NumberSetAttributeMarshaller { + + private static final NumberSetToNumberSetMarshaller INSTANCE = + new NumberSetToNumberSetMarshaller(); + + public static NumberSetToNumberSetMarshaller instance() { + return INSTANCE; + } + + private NumberSetToNumberSetMarshaller() { + } + + @Override + public AttributeValue marshall(Object obj) { + @SuppressWarnings("unchecked") + Set numbers = (Set) obj; + List numberAttributes = new ArrayList(numbers.size()); + + for (Number n : numbers) { + numberAttributes.add(n.toString()); + } + + return AttributeValue.builder().ns(numberAttributes).build(); + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/NumberToNumberMarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/NumberToNumberMarshaller.java new file mode 100644 index 000000000000..ded95e735275 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/NumberToNumberMarshaller.java @@ -0,0 +1,43 @@ +/* + * 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.mapper.dynamodb.internal.marshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.mapper.dynamodb.ArgumentMarshaller.NumberAttributeMarshaller; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * A marshaller that marshals any Java {@code Number} to a DynamoDB number. + */ +@SdkInternalApi +public class NumberToNumberMarshaller implements NumberAttributeMarshaller { + + private static final NumberToNumberMarshaller INSTANCE = + new NumberToNumberMarshaller(); + + public static NumberToNumberMarshaller instance() { + return INSTANCE; + } + + private NumberToNumberMarshaller() { + } + + @Override + public AttributeValue marshall(Object obj) { + Number number = (Number) obj; + return AttributeValue.builder().n(number.toString()).build(); + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/ObjectSetToStringSetMarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/ObjectSetToStringSetMarshaller.java new file mode 100644 index 000000000000..2b5f077b5a9b --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/ObjectSetToStringSetMarshaller.java @@ -0,0 +1,69 @@ +/* + * 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.mapper.dynamodb.internal.marshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import software.amazon.awssdk.utils.Logger; + +import software.amazon.awssdk.mapper.dynamodb.ArgumentMarshaller.StringSetAttributeMarshaller; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * A legacy marshaller that marshals sets of arbitrary Java objects into + * DynamoDB StringSets by using {@link String#valueOf(Object)}. Retained for + * backwards compatibility in case someone is relying on this, but logs a + * warning if ever used since we only know how to unmarshal back to Java + * Strings. + */ +@SdkInternalApi +public class ObjectSetToStringSetMarshaller + implements StringSetAttributeMarshaller { + + private static final Logger LOG = Logger.loggerFor(ObjectSetToStringSetMarshaller.class); + + private static final ObjectSetToStringSetMarshaller INSTANCE = + new ObjectSetToStringSetMarshaller(); + + public static ObjectSetToStringSetMarshaller instance() { + return INSTANCE; + } + + private ObjectSetToStringSetMarshaller() { + } + + @Override + public AttributeValue marshall(Object obj) { + Set set = (Set) obj; + + LOG.warn(() -> "Marshaling a set of non-String objects to a DynamoDB " + + "StringSet. You won't be able to read these objects back " + + "out of DynamoDB unless you REALLY know what you're doing: " + + "it's probably a bug. If you DO know what you're doing feel" + + "free to ignore this warning, but consider using a custom " + + "marshaler for this instead."); + + List strings = new ArrayList(set.size()); + for (Object o : set) { + strings.add(String.valueOf(o)); + } + + return AttributeValue.builder().ss(strings).build(); + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/ObjectToMapMarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/ObjectToMapMarshaller.java new file mode 100644 index 000000000000..8a524b47ec26 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/ObjectToMapMarshaller.java @@ -0,0 +1,53 @@ +/* + * 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.mapper.dynamodb.internal.marshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import java.util.Map; + +import software.amazon.awssdk.mapper.dynamodb.ArgumentMarshaller.MapAttributeMarshaller; +import software.amazon.awssdk.mapper.dynamodb.ItemConverter; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +@SdkInternalApi +public class ObjectToMapMarshaller implements MapAttributeMarshaller { + + private static final ObjectToMapMarshaller INSTANCE = + new ObjectToMapMarshaller(); + + public static ObjectToMapMarshaller instance() { + return INSTANCE; + } + + private final ItemConverter converter; + + private ObjectToMapMarshaller() { + converter = null; + } + + public ObjectToMapMarshaller(ItemConverter converter) { + if (converter == null) { + throw new NullPointerException("converter"); + } + this.converter = converter; + } + + @Override + public AttributeValue marshall(Object obj) { + Map values = converter.convert(obj); + return AttributeValue.builder().m(values).build(); + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/ObjectToStringMarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/ObjectToStringMarshaller.java new file mode 100644 index 000000000000..ad4b493b4c95 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/ObjectToStringMarshaller.java @@ -0,0 +1,45 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.mapper.dynamodb.internal.marshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.mapper.dynamodb.ArgumentMarshaller.StringAttributeMarshaller; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** +* A marshaller that marshals Java {@code Object} objects into DynamoDB +* Strings. +* +* @author Sergei Egorov +*/ +@SdkInternalApi +public class ObjectToStringMarshaller implements StringAttributeMarshaller { + + private static final ObjectToStringMarshaller INSTANCE = + new ObjectToStringMarshaller(); + + public static ObjectToStringMarshaller instance() { + return INSTANCE; + } + + private ObjectToStringMarshaller() { + } + + @Override + public AttributeValue marshall(Object obj) { + return AttributeValue.builder().s(obj.toString()).build(); + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/S3LinkToStringMarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/S3LinkToStringMarshaller.java new file mode 100644 index 000000000000..3c1d5708784e --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/S3LinkToStringMarshaller.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.mapper.dynamodb.internal.marshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.mapper.dynamodb.ArgumentMarshaller.StringAttributeMarshaller; +import software.amazon.awssdk.mapper.dynamodb.S3Link; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * A marshaller that marshals {@code S3Link} objects to DynamoDB Strings, + * using a JSON encoding. For example: {"s3":{"region":"us-west-2", + * "bucket":"my-bucket-name", "key": "foo/bar/baz.txt"}}. + */ +@SdkInternalApi +public class S3LinkToStringMarshaller implements StringAttributeMarshaller { + + private static final S3LinkToStringMarshaller INSTANCE = + new S3LinkToStringMarshaller(); + + public static S3LinkToStringMarshaller instance() { + return INSTANCE; + } + + private S3LinkToStringMarshaller() { + } + + @Override + public AttributeValue marshall(Object obj) { + S3Link s3link = (S3Link) obj; + + if ( s3link.getBucketName() == null || s3link.getKey() == null ) { + // insufficient S3 resource specification + return null; + } + + return AttributeValue.builder().s(s3link.toJson()).build(); + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/StringSetToStringSetMarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/StringSetToStringSetMarshaller.java new file mode 100644 index 000000000000..a2e22dd6ad95 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/StringSetToStringSetMarshaller.java @@ -0,0 +1,56 @@ +/* + * 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.mapper.dynamodb.internal.marshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import software.amazon.awssdk.mapper.dynamodb.ArgumentMarshaller.StringSetAttributeMarshaller; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * A marshaller that marshals sets of Java {@code String}s to DynamoDB + * StringSets. + */ +@SdkInternalApi +public class StringSetToStringSetMarshaller + implements StringSetAttributeMarshaller { + + private static final StringSetToStringSetMarshaller INSTANCE = + new StringSetToStringSetMarshaller(); + + public static StringSetToStringSetMarshaller instance() { + return INSTANCE; + } + + private StringSetToStringSetMarshaller() { + } + + @Override + public AttributeValue marshall(Object obj) { + @SuppressWarnings("unchecked") + Set set = (Set) obj; + + List strings = new ArrayList(set.size()); + for (String s : set) { + strings.add(s); + } + + return AttributeValue.builder().ss(strings).build(); + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/StringToStringMarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/StringToStringMarshaller.java new file mode 100644 index 000000000000..8d5c37035364 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/StringToStringMarshaller.java @@ -0,0 +1,48 @@ +/* + * 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.mapper.dynamodb.internal.marshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.mapper.dynamodb.ArgumentMarshaller.StringAttributeMarshaller; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * A marshaller that marshals Java {@code String} objects to DynamoDB Strings. + */ +@SdkInternalApi +public class StringToStringMarshaller implements StringAttributeMarshaller { + + private static final StringToStringMarshaller INSTANCE = + new StringToStringMarshaller(); + + public static StringToStringMarshaller instance() { + return INSTANCE; + } + + private StringToStringMarshaller() { + } + + @Override + public AttributeValue marshall(Object obj) { + String string = (String) obj; + if (string.length() == 0) { + // Sticking with the legacy behavior for now. + return null; + } + + return AttributeValue.builder().s(string).build(); + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/UUIDSetToStringSetMarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/UUIDSetToStringSetMarshaller.java new file mode 100644 index 000000000000..c7b4de403387 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/UUIDSetToStringSetMarshaller.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.mapper.dynamodb.internal.marshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +import software.amazon.awssdk.mapper.dynamodb.ArgumentMarshaller.StringSetAttributeMarshaller; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * A marshaller that marshals sets of Java {@code Object} objects into + * DynamoDB StringSets. + * + * @author Sergei Egorov + */ +@SdkInternalApi +public class UUIDSetToStringSetMarshaller + implements StringSetAttributeMarshaller { + + private static final UUIDSetToStringSetMarshaller INSTANCE = + new UUIDSetToStringSetMarshaller(); + + public static UUIDSetToStringSetMarshaller instance() { + return INSTANCE; + } + + private UUIDSetToStringSetMarshaller() { + } + + @Override + public AttributeValue marshall(Object obj) { + @SuppressWarnings("unchecked") + Set uuids = (Set) obj; + + List strings = new ArrayList(uuids.size()); + for (UUID uuid : uuids) { + strings.add(uuid.toString()); + } + + return AttributeValue.builder().ss(strings).build(); + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/package-info.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/package-info.java new file mode 100644 index 000000000000..92318fb6b3d1 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/marshallers/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + + +/** + * Standard implementations of {@code ArgumentMarshaller}. + */ +package software.amazon.awssdk.mapper.dynamodb.internal.marshallers; diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/BSUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/BSUnmarshaller.java new file mode 100644 index 000000000000..be9d65eb7e09 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/BSUnmarshaller.java @@ -0,0 +1,35 @@ +/* + * 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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import java.lang.reflect.Method; + +import software.amazon.awssdk.mapper.dynamodb.ArgumentUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMappingException; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +@SdkInternalApi +abstract class BSUnmarshaller implements ArgumentUnmarshaller { + + @Override + public void typeCheck(AttributeValue value, Method setter) { + if ( !value.hasBs() ) { + throw new DynamoDBMappingException("Expected BS in value " + value + " when invoking " + setter); + } + } + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/BUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/BUnmarshaller.java new file mode 100644 index 000000000000..05ee5a9db002 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/BUnmarshaller.java @@ -0,0 +1,35 @@ +/* + * 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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import java.lang.reflect.Method; + +import software.amazon.awssdk.mapper.dynamodb.ArgumentUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMappingException; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +@SdkInternalApi +abstract class BUnmarshaller implements ArgumentUnmarshaller { + + @Override + public void typeCheck(AttributeValue value, Method setter) { + if ( value.b() == null ) { + throw new DynamoDBMappingException("Expected B in value " + value + " when invoking " + setter); + } + } + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/BigDecimalSetUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/BigDecimalSetUnmarshaller.java new file mode 100644 index 000000000000..e1738472f479 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/BigDecimalSetUnmarshaller.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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import java.math.BigDecimal; +import java.util.HashSet; +import java.util.Set; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * An unmarshaller that unmarshals DynamoDB NumberSets into sets of Java + * {@code BigDecimal}s. + */ +@SdkInternalApi +public class BigDecimalSetUnmarshaller extends NSUnmarshaller { + + private static final BigDecimalSetUnmarshaller INSTANCE = + new BigDecimalSetUnmarshaller(); + + public static BigDecimalSetUnmarshaller instance() { + return INSTANCE; + } + + private BigDecimalSetUnmarshaller() { + } + + @Override + public Object unmarshall(AttributeValue value) { + Set result = new HashSet(); + for (String s : value.ns()) { + result.add(new BigDecimal(s)); + } + return result; + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/BigDecimalUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/BigDecimalUnmarshaller.java new file mode 100644 index 000000000000..6e9e99e4b55d --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/BigDecimalUnmarshaller.java @@ -0,0 +1,44 @@ +/* + * 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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import java.math.BigDecimal; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * An unmarshaller that unmarshals DynamoDB Numbers into Java + * {@code BigDecimal}s. + */ +@SdkInternalApi +public class BigDecimalUnmarshaller extends NUnmarshaller { + + private static final BigDecimalUnmarshaller INSTANCE = + new BigDecimalUnmarshaller(); + + public static BigDecimalUnmarshaller instance() { + return INSTANCE; + } + + private BigDecimalUnmarshaller() { + } + + @Override + public Object unmarshall(AttributeValue value) { + return new BigDecimal(value.n()); + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/BigIntegerSetUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/BigIntegerSetUnmarshaller.java new file mode 100644 index 000000000000..df90f632a47c --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/BigIntegerSetUnmarshaller.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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import java.math.BigInteger; +import java.util.HashSet; +import java.util.Set; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * An unmarshaller that unmarshals DynamoDB NumberSets into sets of Java + * {@code BigInteger}s. + */ +@SdkInternalApi +public class BigIntegerSetUnmarshaller extends NSUnmarshaller { + + private static final BigIntegerSetUnmarshaller INSTANCE = + new BigIntegerSetUnmarshaller(); + + public static BigIntegerSetUnmarshaller instance() { + return INSTANCE; + } + + private BigIntegerSetUnmarshaller() { + } + + @Override + public Object unmarshall(AttributeValue value) { + Set result = new HashSet(); + for (String s : value.ns()) { + result.add(new BigInteger(s)); + } + return result; + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/BigIntegerUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/BigIntegerUnmarshaller.java new file mode 100644 index 000000000000..4079f8db39ae --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/BigIntegerUnmarshaller.java @@ -0,0 +1,44 @@ +/* + * 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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import java.math.BigInteger; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * An unmarshaller that unmarshals DynamoDB Numbers into Java + * {@code BigInteger}s. + */ +@SdkInternalApi +public class BigIntegerUnmarshaller extends NUnmarshaller { + + private static final BigIntegerUnmarshaller INSTANCE = + new BigIntegerUnmarshaller(); + + public static BigIntegerUnmarshaller instance() { + return INSTANCE; + } + + private BigIntegerUnmarshaller() { + } + + @Override + public Object unmarshall(AttributeValue value) { + return new BigInteger(value.n()); + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/BooleanSetUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/BooleanSetUnmarshaller.java new file mode 100644 index 000000000000..4637674c4b41 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/BooleanSetUnmarshaller.java @@ -0,0 +1,105 @@ +/* + * 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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import java.lang.reflect.Method; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import software.amazon.awssdk.mapper.dynamodb.ArgumentUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMappingException; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * A special unmarshaller for Set<Boolean>, which the V1 schema stores as + * an NS using 0/1 for true/false. In the V2 schema these fall through to + * the {@code ObjectSetToListMarshaller} which stores them as an L or BOOLs. + */ +@SdkInternalApi +public class BooleanSetUnmarshaller implements ArgumentUnmarshaller { + + private static final BooleanSetUnmarshaller INSTANCE = + new BooleanSetUnmarshaller(); + + public static BooleanSetUnmarshaller instance() { + return INSTANCE; + } + + private BooleanSetUnmarshaller() { + } + + @Override + public void typeCheck(AttributeValue value, Method setter) { + if (!value.hasNs() && !value.hasL()) { + throw new DynamoDBMappingException( + "Expected either L or NS in value " + value + + " when invoking " + setter); + } + } + + @Override + public Object unmarshall(AttributeValue value) { + if (value.hasL()) { + return unmarshallList(value.l()); + } else { + return unmarshallNS(value.ns()); + } + } + + private Set unmarshallList(List values) { + Set result = new HashSet(); + + for (AttributeValue value : values) { + Boolean bool; + if (Boolean.TRUE.equals(value.nul())) { + bool = null; + } else { + bool = value.bool(); + if (bool == null) { + throw new DynamoDBMappingException( + value + " is not a boolean"); + } + } + + if (!result.add(bool)) { + throw new DynamoDBMappingException( + "Duplicate value (" + bool + ") found in " + + values); + } + } + + return result; + } + + private Set unmarshallNS(List values) { + Set result = new HashSet(); + + for (String s : values) { + if ("1".equals(s)) { + result.add(Boolean.TRUE); + } else if ("0".equals(s)) { + result.add(Boolean.FALSE); + } else { + throw new IllegalArgumentException( + "Expected '1' or '0' for boolean value, was " + s); + } + } + + return result; + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/BooleanUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/BooleanUnmarshaller.java new file mode 100644 index 000000000000..a59c166aeaf8 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/BooleanUnmarshaller.java @@ -0,0 +1,70 @@ +/* + * 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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import java.lang.reflect.Method; + +import software.amazon.awssdk.mapper.dynamodb.ArgumentUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMappingException; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * An unmarshaller that unmarshals DynamoDB Bools (or Numbers) into Java + * {@code Boolean}s. Numbers are handled for backwards compatibility with + * versions of the mapper written before the DynamoDB native Boolean type + * was added, which stored Java {@code Boolean}s as either the Number 0 (false) + * or 1 (true). + */ +@SdkInternalApi +public class BooleanUnmarshaller implements ArgumentUnmarshaller { + + private static final BooleanUnmarshaller INSTANCE = + new BooleanUnmarshaller(); + + public static BooleanUnmarshaller instance() { + return INSTANCE; + } + + private BooleanUnmarshaller() { + } + + @Override + public void typeCheck(AttributeValue value, Method setter) { + if (value.n() == null && value.bool() == null) { + throw new DynamoDBMappingException( + "Expected either N or BOOL in value " + value + + " when invoking " + setter); + } + } + + @Override + public Object unmarshall(AttributeValue value) { + if (value.bool() != null) { + return value.bool(); + } + if ("1".equals(value.n())) { + return Boolean.TRUE; + } + if ("0".equals(value.n())) { + return Boolean.FALSE; + } + + throw new IllegalArgumentException( + "Expected '1', '0', or BOOL value for boolean value, was " + + value); + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/ByteArraySetUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/ByteArraySetUnmarshaller.java new file mode 100644 index 000000000000..7aeb911e9c47 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/ByteArraySetUnmarshaller.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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import java.util.HashSet; +import java.util.Set; + +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * An unmarshaller that unmarshals BinarySet values as sets of Java + * {@code byte[]}s. + */ +@SdkInternalApi +public class ByteArraySetUnmarshaller extends BSUnmarshaller { + + private static final ByteArraySetUnmarshaller INSTANCE = + new ByteArraySetUnmarshaller(); + + public static ByteArraySetUnmarshaller instance() { + return INSTANCE; + } + + private ByteArraySetUnmarshaller() { + } + + @Override + public Object unmarshall(AttributeValue value) { + Set result = new HashSet(); + + for (SdkBytes sdkBytes : value.bs()) { + result.add(sdkBytes.asByteArray()); + } + + return result; + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/ByteArrayUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/ByteArrayUnmarshaller.java new file mode 100644 index 000000000000..0ffdd99c6d3c --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/ByteArrayUnmarshaller.java @@ -0,0 +1,41 @@ +/* + * 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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * An unmarshaller that unmarshals Binary values as Java {@code byte[]}s. + */ +@SdkInternalApi +public class ByteArrayUnmarshaller extends BUnmarshaller { + + private static final ByteArrayUnmarshaller INSTANCE = + new ByteArrayUnmarshaller(); + + public static ByteArrayUnmarshaller instance() { + return INSTANCE; + } + + private ByteArrayUnmarshaller() { + } + + @Override + public Object unmarshall(AttributeValue value) { + return value.b().asByteArray(); + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/ByteBufferSetUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/ByteBufferSetUnmarshaller.java new file mode 100644 index 000000000000..4ff40c9b4fac --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/ByteBufferSetUnmarshaller.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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import java.nio.ByteBuffer; +import java.util.HashSet; +import java.util.Set; + +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.mapper.dynamodb.internal.MapperBinaryUtils; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * An unmarshaller that unmarshals BinarySet values as sets of Java + * {@code ByteBuffer}s. + */ +@SdkInternalApi +public class ByteBufferSetUnmarshaller extends BSUnmarshaller { + + private static final ByteBufferSetUnmarshaller INSTANCE = + new ByteBufferSetUnmarshaller(); + + public static ByteBufferSetUnmarshaller instance() { + return INSTANCE; + } + + private ByteBufferSetUnmarshaller() { + } + + @Override + public Object unmarshall(AttributeValue value) { + Set result = new HashSet(); + for (SdkBytes sdkBytes : value.bs()) { + result.add(MapperBinaryUtils.toWritableByteBuffer(sdkBytes)); + } + return result; + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/ByteBufferUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/ByteBufferUnmarshaller.java new file mode 100644 index 000000000000..19f4c4d9132f --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/ByteBufferUnmarshaller.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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.mapper.dynamodb.internal.MapperBinaryUtils; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * An unmarshaller that unmarshals Binary values as Java {@code ByteBuffer}s. + */ +@SdkInternalApi +public class ByteBufferUnmarshaller extends BUnmarshaller { + + private static final ByteBufferUnmarshaller INSTANCE = + new ByteBufferUnmarshaller(); + + public static ByteBufferUnmarshaller instance() { + return INSTANCE; + } + + private ByteBufferUnmarshaller() { + } + + @Override + public Object unmarshall(AttributeValue value) { + return MapperBinaryUtils.toWritableByteBuffer(value.b()); + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/ByteSetUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/ByteSetUnmarshaller.java new file mode 100644 index 000000000000..842fb329181d --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/ByteSetUnmarshaller.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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import java.util.HashSet; +import java.util.Set; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * An unmarshaller that unmarshals DynamoDB NumberSets into sets of Java + * {@code Byte}s. + */ +@SdkInternalApi +public class ByteSetUnmarshaller extends NSUnmarshaller { + + private static final ByteSetUnmarshaller INSTANCE = + new ByteSetUnmarshaller(); + + public static ByteSetUnmarshaller instance() { + return INSTANCE; + } + + private ByteSetUnmarshaller() { + } + + @Override + public Object unmarshall(AttributeValue value) { + Set result = new HashSet(); + for (String s : value.ns()) { + result.add(Byte.valueOf(s)); + } + return result; + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/ByteUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/ByteUnmarshaller.java new file mode 100644 index 000000000000..0ef61768eeaf --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/ByteUnmarshaller.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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * An unmarshaller that unmarshals DynamoDB Numbers into Java + * {@code Byte}s. + */ +@SdkInternalApi +public class ByteUnmarshaller extends NUnmarshaller { + + private static final ByteUnmarshaller INSTANCE = + new ByteUnmarshaller(); + + public static ByteUnmarshaller instance() { + return INSTANCE; + } + + private ByteUnmarshaller() { + } + + @Override + public Object unmarshall(AttributeValue value) { + return Byte.valueOf(value.n()); + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/CalendarSetUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/CalendarSetUnmarshaller.java new file mode 100644 index 000000000000..a284706dba7d --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/CalendarSetUnmarshaller.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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.mapper.dynamodb.internal.MapperDateUtils; + +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.HashSet; +import java.util.Set; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * An unmarshaller that unmarshals sets of ISO-8601-formatted dates as sets of + * Java {@code Calendar} objects. + */ +@SdkInternalApi +public class CalendarSetUnmarshaller extends SSUnmarshaller { + + private static final CalendarSetUnmarshaller INSTANCE = + new CalendarSetUnmarshaller(); + + public static CalendarSetUnmarshaller instance() { + return INSTANCE; + } + + private CalendarSetUnmarshaller() { + } + + @Override + public Object unmarshall(AttributeValue value) { + Set result = new HashSet(); + + for (String s : value.ss()) { + Calendar cal = GregorianCalendar.getInstance(); + cal.setTime(MapperDateUtils.parseISO8601Date(s)); + result.add(cal); + } + + return result; + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/CalendarUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/CalendarUnmarshaller.java new file mode 100644 index 000000000000..c6f7a61ec284 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/CalendarUnmarshaller.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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.mapper.dynamodb.internal.MapperDateUtils; + +import java.util.Calendar; +import java.util.GregorianCalendar; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * An unmarshaller that unmarshals ISO-8601-formatted dates as Java + * {@code Calendar} objects. + */ +@SdkInternalApi +public class CalendarUnmarshaller extends SUnmarshaller { + + private static final CalendarUnmarshaller INSTANCE = + new CalendarUnmarshaller(); + + public static CalendarUnmarshaller instance() { + return INSTANCE; + } + + private CalendarUnmarshaller() { + } + + @Override + public Object unmarshall(AttributeValue value) { + Calendar cal = GregorianCalendar.getInstance(); + cal.setTime(MapperDateUtils.parseISO8601Date(value.s())); + return cal; + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/CustomUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/CustomUnmarshaller.java new file mode 100644 index 000000000000..fc10fd7c5dc4 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/CustomUnmarshaller.java @@ -0,0 +1,72 @@ +/* + * 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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMappingException; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMarshaller; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * An unmarshaller that delegates to an instance of a + * {@code DynamoDBMarshaller}-derived custom marshaler. + */ +@SdkInternalApi +public class CustomUnmarshaller extends SUnmarshaller { + + private final Class targetClass; + private final Class> unmarshallerClass; + + public CustomUnmarshaller( + Class targetClass, + Class> unmarshallerClass) { + + this.targetClass = targetClass; + this.unmarshallerClass = unmarshallerClass; + } + + @Override + @SuppressWarnings({ "rawtypes", "unchecked" }) + public Object unmarshall(AttributeValue value) { + + // TODO: Would be nice to cache this object, but not sure if we can + // do that now without a breaking change; user's unmarshallers + // might not all be thread-safe. + + DynamoDBMarshaller unmarshaller = + createUnmarshaller(unmarshallerClass); + + return unmarshaller.unmarshall(targetClass, value.s()); + } + + @SuppressWarnings({ "rawtypes" }) + private static DynamoDBMarshaller createUnmarshaller(Class clazz) { + try { + + return (DynamoDBMarshaller) clazz.newInstance(); + + } catch (InstantiationException e) { + throw new DynamoDBMappingException( + "Failed to instantiate custom marshaler for class " + clazz, + e); + + } catch (IllegalAccessException e) { + throw new DynamoDBMappingException( + "Failed to instantiate custom marshaler for class " + clazz, + e); + } + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/DateSetUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/DateSetUnmarshaller.java new file mode 100644 index 000000000000..dae8eb350f39 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/DateSetUnmarshaller.java @@ -0,0 +1,54 @@ +/* + * 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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.mapper.dynamodb.internal.MapperDateUtils; + +import java.util.Date; +import java.util.HashSet; +import java.util.Set; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * An unmarshaller that unmarshals sets of ISO-8601-formatted dates as sets of + * Java {@code Date} objects. + */ +@SdkInternalApi +public class DateSetUnmarshaller extends SSUnmarshaller { + + private static final DateSetUnmarshaller INSTANCE = + new DateSetUnmarshaller(); + + public static DateSetUnmarshaller instance() { + return INSTANCE; + } + + private DateSetUnmarshaller() { + } + + @Override + public Object unmarshall(AttributeValue value) { + Set result = new HashSet(); + + for (String s : value.ss()) { + result.add(MapperDateUtils.parseISO8601Date(s)); + } + + return result; + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/DateUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/DateUnmarshaller.java new file mode 100644 index 000000000000..409bd21895bc --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/DateUnmarshaller.java @@ -0,0 +1,44 @@ +/* + * 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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.mapper.dynamodb.internal.MapperDateUtils; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * An unmarshaller that unmarshals ISO-8601-formatted dates as Java + * {@code Date} objects. + */ +@SdkInternalApi +public class DateUnmarshaller extends SUnmarshaller { + + private static final DateUnmarshaller INSTANCE = + new DateUnmarshaller(); + + public static DateUnmarshaller instance() { + return INSTANCE; + } + + private DateUnmarshaller() { + } + + @Override + public Object unmarshall(AttributeValue value) { + return MapperDateUtils.parseISO8601Date(value.s()); + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/DoubleSetUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/DoubleSetUnmarshaller.java new file mode 100644 index 000000000000..c786d01a7240 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/DoubleSetUnmarshaller.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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import java.util.HashSet; +import java.util.Set; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * An unmarshaller that unmarshals DynamoDB NumberSets into sets of Java + * {@code Double}s. + */ +@SdkInternalApi +public class DoubleSetUnmarshaller extends NSUnmarshaller { + + private static final DoubleSetUnmarshaller INSTANCE = + new DoubleSetUnmarshaller(); + + public static DoubleSetUnmarshaller instance() { + return INSTANCE; + } + + private DoubleSetUnmarshaller() { + } + + @Override + public Object unmarshall(AttributeValue value) { + Set result = new HashSet(); + for (String s : value.ns()) { + result.add(Double.valueOf(s)); + } + return result; + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/DoubleUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/DoubleUnmarshaller.java new file mode 100644 index 000000000000..e3f136f81412 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/DoubleUnmarshaller.java @@ -0,0 +1,41 @@ +/* + * 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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * An unmarshaller that unmarshals DynamoDB Numbers into Java {@code Double}s. + */ +@SdkInternalApi +public class DoubleUnmarshaller extends NUnmarshaller { + + private static final DoubleUnmarshaller INSTANCE = + new DoubleUnmarshaller(); + + public static DoubleUnmarshaller instance() { + return INSTANCE; + } + + private DoubleUnmarshaller() { + } + + @Override + public Object unmarshall(AttributeValue value) { + return Double.valueOf(value.n()); + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/FloatSetUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/FloatSetUnmarshaller.java new file mode 100644 index 000000000000..cf3962395270 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/FloatSetUnmarshaller.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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import java.util.HashSet; +import java.util.Set; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * An unmarshaller that unmarshals DynamoDB NumberSets into sets of Java + * {@code Float}s. + */ +@SdkInternalApi +public class FloatSetUnmarshaller extends NSUnmarshaller { + + private static final FloatSetUnmarshaller INSTANCE = + new FloatSetUnmarshaller(); + + public static FloatSetUnmarshaller instance() { + return INSTANCE; + } + + private FloatSetUnmarshaller() { + } + + @Override + public Object unmarshall(AttributeValue value) { + Set result = new HashSet(); + for (String s : value.ns()) { + result.add(Float.valueOf(s)); + } + return result; + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/FloatUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/FloatUnmarshaller.java new file mode 100644 index 000000000000..281204ea812c --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/FloatUnmarshaller.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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * An unmarshaller that unmarshals DynamoDB Numbers into Java + * {@code Float}s. + */ +@SdkInternalApi +public class FloatUnmarshaller extends NUnmarshaller { + + private static final FloatUnmarshaller INSTANCE = + new FloatUnmarshaller(); + + public static FloatUnmarshaller instance() { + return INSTANCE; + } + + private FloatUnmarshaller() { + } + + @Override + public Object unmarshall(AttributeValue value) { + return Float.valueOf(value.n()); + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/IntegerSetUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/IntegerSetUnmarshaller.java new file mode 100644 index 000000000000..aa3076f19263 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/IntegerSetUnmarshaller.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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import java.util.HashSet; +import java.util.Set; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * An unmarshaller that unmarshals DynamoDB NumberSets into sets of Java + * {@code Integer}s. + */ +@SdkInternalApi +public class IntegerSetUnmarshaller extends NSUnmarshaller { + + private static final IntegerSetUnmarshaller INSTANCE = + new IntegerSetUnmarshaller(); + + public static IntegerSetUnmarshaller instance() { + return INSTANCE; + } + + private IntegerSetUnmarshaller() { + } + + @Override + public Object unmarshall(AttributeValue value) { + Set result = new HashSet(); + for (String s : value.ns()) { + result.add(Integer.valueOf(s)); + } + return result; + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/IntegerUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/IntegerUnmarshaller.java new file mode 100644 index 000000000000..2284a654d8c4 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/IntegerUnmarshaller.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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * An unmarshaller that unmarshals DynamoDB Numbers into Java + * {@code Integer}s. + */ +@SdkInternalApi +public class IntegerUnmarshaller extends NUnmarshaller { + + private static final IntegerUnmarshaller INSTANCE = + new IntegerUnmarshaller(); + + public static IntegerUnmarshaller instance() { + return INSTANCE; + } + + private IntegerUnmarshaller() { + } + + @Override + public Object unmarshall(AttributeValue value) { + return Integer.valueOf(value.n()); + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/LUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/LUnmarshaller.java new file mode 100644 index 000000000000..239b8feecc77 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/LUnmarshaller.java @@ -0,0 +1,35 @@ +/* + * 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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import java.lang.reflect.Method; + +import software.amazon.awssdk.mapper.dynamodb.ArgumentUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMappingException; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +@SdkInternalApi +abstract class LUnmarshaller implements ArgumentUnmarshaller { + + @Override + public void typeCheck(AttributeValue value, Method setter) { + if ( !value.hasL() ) { + throw new DynamoDBMappingException("Expected L in value " + value + " when invoking " + setter); + } + } + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/ListUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/ListUnmarshaller.java new file mode 100644 index 000000000000..dcba1dae421a --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/ListUnmarshaller.java @@ -0,0 +1,63 @@ +/* + * 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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.List; + +import software.amazon.awssdk.mapper.dynamodb.ArgumentUnmarshaller; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * An unmarshaller that unmarshals Lists into Java {@code Lists}. + */ +@SdkInternalApi +public class ListUnmarshaller extends LUnmarshaller { + + private static final ListUnmarshaller INSTANCE = new ListUnmarshaller(); + + public static ListUnmarshaller instance() { + return INSTANCE; + } + + private final ArgumentUnmarshaller memberUnmarshaller; + + private ListUnmarshaller() { + memberUnmarshaller = null; + } + + public ListUnmarshaller(ArgumentUnmarshaller memberUnmarshaller) { + if (memberUnmarshaller == null) { + throw new NullPointerException("memberUnmarshaller"); + } + this.memberUnmarshaller = memberUnmarshaller; + } + + @Override + public Object unmarshall(AttributeValue value) throws ParseException { + List values = value.l(); + List objects = new ArrayList(values.size()); + + for (AttributeValue v : values) { + memberUnmarshaller.typeCheck(v, null); + objects.add(memberUnmarshaller.unmarshall(v)); + } + + return objects; + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/LongSetUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/LongSetUnmarshaller.java new file mode 100644 index 000000000000..92d5da13d581 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/LongSetUnmarshaller.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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import java.util.HashSet; +import java.util.Set; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * An unmarshaller that unmarshals DynamoDB NumberSets into sets of Java + * {@code Long}s. + */ +@SdkInternalApi +public class LongSetUnmarshaller extends NSUnmarshaller { + + private static final LongSetUnmarshaller INSTANCE = + new LongSetUnmarshaller(); + + public static LongSetUnmarshaller instance() { + return INSTANCE; + } + + private LongSetUnmarshaller() { + } + + @Override + public Object unmarshall(AttributeValue value) { + Set result = new HashSet(); + for (String s : value.ns()) { + result.add(Long.valueOf(s)); + } + return result; + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/LongUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/LongUnmarshaller.java new file mode 100644 index 000000000000..4c2b2d5cf410 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/LongUnmarshaller.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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * An unmarshaller that unmarshals DynamoDB Numbers into Java + * {@code Long}s. + */ +@SdkInternalApi +public class LongUnmarshaller extends NUnmarshaller { + + private static final LongUnmarshaller INSTANCE = + new LongUnmarshaller(); + + public static LongUnmarshaller instance() { + return INSTANCE; + } + + private LongUnmarshaller() { + } + + @Override + public Object unmarshall(AttributeValue value) { + return Long.valueOf(value.n()); + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/MUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/MUnmarshaller.java new file mode 100644 index 000000000000..1b2827c87144 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/MUnmarshaller.java @@ -0,0 +1,35 @@ +/* + * 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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import java.lang.reflect.Method; + +import software.amazon.awssdk.mapper.dynamodb.ArgumentUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMappingException; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +@SdkInternalApi +abstract class MUnmarshaller implements ArgumentUnmarshaller { + + @Override + public void typeCheck(AttributeValue value, Method setter) { + if ( !value.hasM() ) { + throw new DynamoDBMappingException("Expected M in value " + value + " when invoking " + setter); + } + } + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/MapUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/MapUnmarshaller.java new file mode 100644 index 000000000000..e1f30a96d21a --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/MapUnmarshaller.java @@ -0,0 +1,61 @@ +/* + * 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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import java.text.ParseException; +import java.util.HashMap; +import java.util.Map; + +import software.amazon.awssdk.mapper.dynamodb.ArgumentUnmarshaller; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +@SdkInternalApi +public class MapUnmarshaller extends MUnmarshaller { + + private static final MapUnmarshaller INSTANCE = new MapUnmarshaller(); + + public static MapUnmarshaller instance() { + return INSTANCE; + } + + private final ArgumentUnmarshaller memberUnmarshaller; + + private MapUnmarshaller() { + memberUnmarshaller = null; + } + + public MapUnmarshaller(ArgumentUnmarshaller memberUnmarshaller) { + if (memberUnmarshaller == null) { + throw new NullPointerException("memberUnmarshaller"); + } + this.memberUnmarshaller = memberUnmarshaller; + } + + @Override + public Object unmarshall(AttributeValue value) throws ParseException { + Map map = value.m(); + Map result = new HashMap(); + + for (Map.Entry entry : map.entrySet()) { + memberUnmarshaller.typeCheck(entry.getValue(), null); + result.put(entry.getKey(), + memberUnmarshaller.unmarshall(entry.getValue())); + } + + return result; + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/NSUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/NSUnmarshaller.java new file mode 100644 index 000000000000..2ab50c34400d --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/NSUnmarshaller.java @@ -0,0 +1,35 @@ +/* + * 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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import java.lang.reflect.Method; + +import software.amazon.awssdk.mapper.dynamodb.ArgumentUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMappingException; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +@SdkInternalApi +abstract class NSUnmarshaller implements ArgumentUnmarshaller { + + @Override + public void typeCheck(AttributeValue value, Method setter) { + if ( !value.hasNs() ) { + throw new DynamoDBMappingException("Expected NS in value " + value + " when invoking " + setter); + } + } + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/NUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/NUnmarshaller.java new file mode 100644 index 000000000000..83710c6b21d2 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/NUnmarshaller.java @@ -0,0 +1,35 @@ +/* + * 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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import java.lang.reflect.Method; + +import software.amazon.awssdk.mapper.dynamodb.ArgumentUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMappingException; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +@SdkInternalApi +abstract class NUnmarshaller implements ArgumentUnmarshaller { + + @Override + public void typeCheck(AttributeValue value, Method setter) { + if ( value.n() == null ) { + throw new DynamoDBMappingException("Expected N in value " + value + " when invoking " + setter); + } + } + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/NullableUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/NullableUnmarshaller.java new file mode 100644 index 000000000000..88b606f78f56 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/NullableUnmarshaller.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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import java.lang.reflect.Method; +import java.text.ParseException; + +import software.amazon.awssdk.mapper.dynamodb.ArgumentUnmarshaller; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +@SdkInternalApi +public class NullableUnmarshaller implements ArgumentUnmarshaller { + + private final ArgumentUnmarshaller wrapped; + + public NullableUnmarshaller(ArgumentUnmarshaller wrapped) { + if (wrapped == null) { + throw new NullPointerException("wrapped"); + } + this.wrapped = wrapped; + } + + @Override + public void typeCheck(AttributeValue value, Method setter) { + if (value.nul() == null) { + wrapped.typeCheck(value, setter); + } + } + + @Override + public Object unmarshall(AttributeValue value) throws ParseException { + if (value.nul() != null) { + return null; + } + return wrapped.unmarshall(value); + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/ObjectSetUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/ObjectSetUnmarshaller.java new file mode 100644 index 000000000000..287f89db0ef1 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/ObjectSetUnmarshaller.java @@ -0,0 +1,70 @@ +/* + * 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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import java.text.ParseException; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import software.amazon.awssdk.mapper.dynamodb.ArgumentUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMappingException; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +@SdkInternalApi +public class ObjectSetUnmarshaller extends LUnmarshaller { + + private static final ObjectSetUnmarshaller INSTANCE = + new ObjectSetUnmarshaller(); + + public static ObjectSetUnmarshaller instance() { + return INSTANCE; + } + + private final ArgumentUnmarshaller memberUnmarshaller; + + private ObjectSetUnmarshaller() { + memberUnmarshaller = null; + } + + public ObjectSetUnmarshaller(ArgumentUnmarshaller memberUnmarshaller) { + if (memberUnmarshaller == null) { + throw new NullPointerException("memberUnmarshaller"); + } + this.memberUnmarshaller = memberUnmarshaller; + } + + @Override + public Object unmarshall(AttributeValue value) throws ParseException { + List values = value.l(); + + // As in the LinkedHashSet(Collection) constructor. + int size = Math.max(values.size() * 2, 11); + Set objects = new LinkedHashSet(size); + + for (AttributeValue v : values) { + memberUnmarshaller.typeCheck(v, null); + Object o = memberUnmarshaller.unmarshall(v); + if (!objects.add(o)) { + throw new DynamoDBMappingException( + "Duplicate value (" + o + ") found in " + values); + } + } + + return objects; + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/ObjectUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/ObjectUnmarshaller.java new file mode 100644 index 000000000000..5d1602a2afa2 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/ObjectUnmarshaller.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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import java.text.ParseException; +import java.util.Map; + +import software.amazon.awssdk.mapper.dynamodb.ItemConverter; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +@SdkInternalApi +public class ObjectUnmarshaller extends MUnmarshaller { + + private static final ObjectUnmarshaller INSTANCE = new ObjectUnmarshaller(); + + public static ObjectUnmarshaller instance() { + return INSTANCE; + } + + private final ItemConverter converter; + private final Class clazz; + + private ObjectUnmarshaller() { + converter = null; + clazz = null; + } + + public ObjectUnmarshaller(ItemConverter converter, Class clazz) { + if (converter == null) { + throw new NullPointerException("converter"); + } + if (clazz == null) { + throw new NullPointerException("clazz"); + } + + this.converter = converter; + this.clazz = clazz; + } + + @Override + public Object unmarshall(AttributeValue value) throws ParseException { + Map map = value.m(); + return converter.unconvert(clazz, map); + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/S3LinkUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/S3LinkUnmarshaller.java new file mode 100644 index 000000000000..2e3d2e6fb8dc --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/S3LinkUnmarshaller.java @@ -0,0 +1,53 @@ +/* + * 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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.mapper.dynamodb.S3ClientCache; +import software.amazon.awssdk.mapper.dynamodb.S3Link; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +@SdkInternalApi +public class S3LinkUnmarshaller extends SUnmarshaller { + + private static final S3LinkUnmarshaller INSTANCE = new S3LinkUnmarshaller(); + + public static S3LinkUnmarshaller instance() { + return INSTANCE; + } + + + private final S3ClientCache clientCache; + + private S3LinkUnmarshaller() { + this(null); + } + + public S3LinkUnmarshaller(S3ClientCache clientCache) { + this.clientCache = clientCache; + } + + @Override + public Object unmarshall(AttributeValue value) { + if (clientCache == null) { + throw new IllegalStateException( + "Mapper must be constructed with S3 AWS Credentials to " + + "load S3Link"); + } + + return S3Link.fromJson(clientCache, value.s()); + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/SSUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/SSUnmarshaller.java new file mode 100644 index 000000000000..371b2225d978 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/SSUnmarshaller.java @@ -0,0 +1,35 @@ +/* + * 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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import java.lang.reflect.Method; + +import software.amazon.awssdk.mapper.dynamodb.ArgumentUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMappingException; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +@SdkInternalApi +abstract class SSUnmarshaller implements ArgumentUnmarshaller { + + @Override + public void typeCheck(AttributeValue value, Method setter) { + if ( !value.hasSs() ) { + throw new DynamoDBMappingException("Expected SS in value " + value + " when invoking " + setter); + } + } + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/SUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/SUnmarshaller.java new file mode 100644 index 000000000000..c5b1367f1005 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/SUnmarshaller.java @@ -0,0 +1,35 @@ +/* + * 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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import java.lang.reflect.Method; + +import software.amazon.awssdk.mapper.dynamodb.ArgumentUnmarshaller; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMappingException; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +@SdkInternalApi +abstract class SUnmarshaller implements ArgumentUnmarshaller { + + @Override + public void typeCheck(AttributeValue value, Method setter) { + if ( value.s() == null ) { + throw new DynamoDBMappingException("Expected S in value " + value + " when invoking " + setter); + } + } + +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/ShortSetUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/ShortSetUnmarshaller.java new file mode 100644 index 000000000000..b4126c461fb7 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/ShortSetUnmarshaller.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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import java.util.HashSet; +import java.util.Set; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * An unmarshaller that unmarshals DynamoDB NumberSets into sets of Java + * {@code Short}s. + */ +@SdkInternalApi +public class ShortSetUnmarshaller extends NSUnmarshaller { + + private static final ShortSetUnmarshaller INSTANCE = + new ShortSetUnmarshaller(); + + public static ShortSetUnmarshaller instance() { + return INSTANCE; + } + + private ShortSetUnmarshaller() { + } + + @Override + public Object unmarshall(AttributeValue value) { + Set result = new HashSet(); + for (String s : value.ns()) { + result.add(Short.valueOf(s)); + } + return result; + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/ShortUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/ShortUnmarshaller.java new file mode 100644 index 000000000000..7cc49f3429b2 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/ShortUnmarshaller.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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * An unmarshaller that unmarshals DynamoDB Numbers into Java + * {@code Short}s. + */ +@SdkInternalApi +public class ShortUnmarshaller extends NUnmarshaller { + + private static final ShortUnmarshaller INSTANCE = + new ShortUnmarshaller(); + + public static ShortUnmarshaller instance() { + return INSTANCE; + } + + private ShortUnmarshaller() { + } + + @Override + public Object unmarshall(AttributeValue value) { + return Short.valueOf(value.n()); + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/StringSetUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/StringSetUnmarshaller.java new file mode 100644 index 000000000000..d4cffdf56993 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/StringSetUnmarshaller.java @@ -0,0 +1,44 @@ +/* + * 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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import java.util.HashSet; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * An unmarshaller that unmarshals DynamoDB StringSets as sets of Java + * {@code String} objects. + */ +@SdkInternalApi +public class StringSetUnmarshaller extends SSUnmarshaller { + + private static final StringSetUnmarshaller INSTANCE = + new StringSetUnmarshaller(); + + public static StringSetUnmarshaller instance() { + return INSTANCE; + } + + private StringSetUnmarshaller() { + } + + @Override + public Object unmarshall(AttributeValue value) { + return new HashSet(value.ss()); + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/StringUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/StringUnmarshaller.java new file mode 100644 index 000000000000..b18476ef0d73 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/StringUnmarshaller.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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * An unmarshaller that unmarshals DynamoDB Strings as Java {@code String} + * objects. + */ +@SdkInternalApi +public class StringUnmarshaller extends SUnmarshaller { + + private static final StringUnmarshaller INSTANCE = + new StringUnmarshaller(); + + public static StringUnmarshaller instance() { + return INSTANCE; + } + + private StringUnmarshaller() { + } + + @Override + public Object unmarshall(AttributeValue value) { + return value.s(); + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/UUIDSetUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/UUIDSetUnmarshaller.java new file mode 100644 index 000000000000..9cb33619f504 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/UUIDSetUnmarshaller.java @@ -0,0 +1,54 @@ +/* + * 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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * An unmarshaller that unmarshals sets of UUIDs as sets of + * Java {@code UUID} objects. + * + * @author Sergei Egorov + */ +@SdkInternalApi +public class UUIDSetUnmarshaller extends SSUnmarshaller { + + private static final UUIDSetUnmarshaller INSTANCE = + new UUIDSetUnmarshaller(); + + public static UUIDSetUnmarshaller instance() { + return INSTANCE; + } + + private UUIDSetUnmarshaller() { + } + + @Override + public Set unmarshall(AttributeValue value) { + Set result = new HashSet(); + + for (String s : value.ss()) { + result.add(UUID.fromString(s)); + } + + return result; + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/UUIDUnmarshaller.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/UUIDUnmarshaller.java new file mode 100644 index 000000000000..72bd00da0990 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/UUIDUnmarshaller.java @@ -0,0 +1,46 @@ +/* + * 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.mapper.dynamodb.internal.unmarshallers; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import java.util.UUID; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * An unmarshaller that unmarshals UUIDs as Java + * {@code UUID} objects. + * + * @author Sergei Egorov + */ +@SdkInternalApi +public class UUIDUnmarshaller extends SUnmarshaller { + + private static final UUIDUnmarshaller INSTANCE = + new UUIDUnmarshaller(); + + public static UUIDUnmarshaller instance() { + return INSTANCE; + } + + private UUIDUnmarshaller() { + } + + @Override + public UUID unmarshall(AttributeValue value) { + return UUID.fromString(value.s()); + } +} diff --git a/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/package-info.java b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/package-info.java new file mode 100644 index 000000000000..2428ecf3d809 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/main/java/software/amazon/awssdk/mapper/dynamodb/internal/unmarshallers/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + + +/** + * Standard implementations of {@code ArgumentUnmarshaller}. + */ +package software.amazon.awssdk.mapper.dynamodb.internal.unmarshallers; diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/AttributeTransformerChainTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/AttributeTransformerChainTest.java new file mode 100644 index 000000000000..96559a83ef94 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/AttributeTransformerChainTest.java @@ -0,0 +1,192 @@ +/* + * 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.mapper.dynamodb; + +import java.util.*; + +import org.junit.*; + +import software.amazon.awssdk.mapper.dynamodb.AttributeTransformer.Parameters; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +public class AttributeTransformerChainTest { + @Test + public void testTransformEmptyChain() { + + AttributeTransformer transformer = new AttributeTransformerChain( + Collections.emptyList() + ); + + Map values = + new HashMap(); + + Parameters params = new TestParameters(values); + + Map result = transformer.transform(params); + Assert.assertSame(values, result); + Assert.assertTrue(values.isEmpty()); + } + + @Test + public void testUntransformEmptyChain() { + + AttributeTransformer transformer = new AttributeTransformerChain( + Collections.emptyList() + ); + + Map values = + new HashMap(); + + Parameters params = new TestParameters(values); + + Map result = transformer.untransform(params); + Assert.assertSame(values, result); + Assert.assertTrue(values.isEmpty()); + } + + @Test + public void testTransform() { + + AttributeTransformer transformer1 = new TestTransformer(".one"); + AttributeTransformer transformer2 = new TestTransformer(".two"); + + AttributeTransformer chain = + new AttributeTransformerChain(transformer1, transformer2); + + Map values = + new HashMap(); + + values.put("test1", AttributeValue.builder().s("foo").build()); + values.put("test2", AttributeValue.builder().s("bar").build()); + + Parameters params = new TestParameters(values); + + Map result = chain.transform(params); + + Assert.assertNotNull(result); + Assert.assertEquals(2, result.size()); + + Assert.assertEquals("foo.one.two", result.get("test1").s()); + Assert.assertEquals("bar.one.two", result.get("test2").s()); + } + + @Test + public void testUntransform() { + + AttributeTransformer transformer1 = new TestTransformer(".one"); + AttributeTransformer transformer2 = new TestTransformer(".two"); + + AttributeTransformer chain = + new AttributeTransformerChain(transformer1, transformer2); + + Map values = + new HashMap(); + + values.put("test1", AttributeValue.builder().s("foo.one.two").build()); + values.put("test2", AttributeValue.builder().s("bar.one.two").build()); + + Parameters params = new TestParameters(values); + + Map result = chain.untransform(params); + + Assert.assertNotNull(result); + Assert.assertEquals(2, result.size()); + + Assert.assertEquals("foo", result.get("test1").s()); + Assert.assertEquals("bar", result.get("test2").s()); + } + + @Test + public void testRoundTrip() { + + AttributeTransformer transformer1 = new TestTransformer(".one"); + AttributeTransformer transformer2 = new TestTransformer(".two"); + + AttributeTransformer chain = + new AttributeTransformerChain(transformer1, transformer2); + + Map values = + new HashMap(); + + values.put("test1", AttributeValue.builder().s("foo").build()); + values.put("test2", AttributeValue.builder().s("bar").build()); + + Parameters params = new TestParameters(values); + + Map result = chain.transform(params); + + params = new TestParameters(result); + + result = chain.untransform(params); + + Assert.assertEquals(values, result); + } + + private static class TestTransformer implements AttributeTransformer { + + private final String appendMe; + + public TestTransformer(final String appendMe) { + this.appendMe = appendMe; + } + + @Override + public Map transform( + final Parameters parameters) { + + Map rval = + new HashMap(); + + for (Map.Entry entry + : parameters.getAttributeValues().entrySet()) { + + rval.put(entry.getKey(), transform(entry.getValue())); + } + + return rval; + } + + @Override + public Map untransform( + final Parameters parameters) { + + Map rval = + new HashMap(); + + for (Map.Entry entry + : parameters.getAttributeValues().entrySet()) { + + rval.put(entry.getKey(), untransform(entry.getValue())); + } + + return rval; + } + + private AttributeValue transform(AttributeValue value) { + return AttributeValue.builder().s(value.s() + appendMe).build(); + } + + private AttributeValue untransform(AttributeValue value) { + String s = value.s(); + if (s.endsWith(appendMe)) { + return AttributeValue.builder().s( + s.substring(0, s.length() - appendMe.length())).build(); + } else { + return value; + } + } + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/BatchLoadRetryStrategyTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/BatchLoadRetryStrategyTest.java new file mode 100644 index 000000000000..f3508dd8c7db --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/BatchLoadRetryStrategyTest.java @@ -0,0 +1,328 @@ +/* + * 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.mapper.dynamodb; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.BatchGetItemRequest; +import software.amazon.awssdk.services.dynamodb.model.BatchGetItemResponse; +import software.amazon.awssdk.services.dynamodb.model.KeysAndAttributes; +import software.amazon.awssdk.services.dynamodb.model.PutRequest; +import software.amazon.awssdk.services.dynamodb.model.WriteRequest; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.BatchLoadRetryStrategy; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper.BatchGetItemException; + +public class BatchLoadRetryStrategyTest { + + private static final String TABLE_NAME = "tableName"; + private static final String TABLE_NAME2 = "tableName2"; + private static final String TABLE_NAME3 = "tableName3"; + private static final String HASH_ATTR = "hash"; + + private static List itemsToGet; + + private DynamoDbClient ddbMock; + private DynamoDBMapper mapper; + + static { + + itemsToGet = new ArrayList(); + itemsToGet.add(new Item3("Bruce Wayne")); + itemsToGet.add(new Item2("Is")); + itemsToGet.add(new Item("Batman")); + } + + @Before + public void setup() { + ddbMock = mock(DynamoDbClient.class); + } + + @Test + public void testBatchReadCallFailure_NoRetry() { + when(ddbMock.batchGetItem(any(BatchGetItemRequest.class))) + .thenReturn(buildGetItemResultWithUnprocessedKeys(1)); + mapper = new DynamoDBMapper(ddbMock, getConfigWithCustomBatchLoadRetryStrategy(new DynamoDBMapperConfig.NoRetryBatchLoadRetryStrategy())); + + assertBatchLoadFails(); + verify(ddbMock, times(1)).batchGetItem(any(BatchGetItemRequest.class)); + } + + @Test + public void testBatchReadCallFailure_Retry() { + when(ddbMock.batchGetItem(any(BatchGetItemRequest.class))) + .thenReturn(buildGetItemResultWithUnprocessedKeys(1)); + mapper = new DynamoDBMapper(ddbMock, getConfigWithCustomBatchLoadRetryStrategy(new BatchLoadRetryStrategyWithNoDelay(3))); + + assertBatchLoadFails(); + verify(ddbMock, times(4)).batchGetItem(any(BatchGetItemRequest.class)); + } + + @Test + public void testBatchReadCallSuccess_Retry() { + when(ddbMock.batchGetItem(any(BatchGetItemRequest.class))) + .thenReturn(buildDefaultGetItemResult() + .toBuilder() + .unprocessedKeys(new HashMap(1)) + .build()); + mapper = new DynamoDBMapper(ddbMock, getConfigWithCustomBatchLoadRetryStrategy(new DynamoDBMapperConfig.DefaultBatchLoadRetryStrategy())); + + mapper.batchLoad(itemsToGet); + verify(ddbMock, times(1)).batchGetItem(any(BatchGetItemRequest.class)); + } + + @Test + public void testBatchReadCallFailure_Retry_RetryOnCompleteFailure() { + when(ddbMock.batchGetItem(any(BatchGetItemRequest.class))) + .thenReturn(buildGetItemResultWithUnprocessedKeys(3)); + mapper = new DynamoDBMapper(ddbMock, getConfigWithCustomBatchLoadRetryStrategy(new DynamoDBMapperConfig.DefaultBatchLoadRetryStrategy())); + + assertBatchLoadFails(); + verify(ddbMock, times(6)).batchGetItem(any(BatchGetItemRequest.class)); + } + + @Test + public void testBatchReadCallFailure_NoRetry_RetryOnCompleteFailure() { + when(ddbMock.batchGetItem(any(BatchGetItemRequest.class))) + .thenReturn(buildGetItemResultWithUnprocessedKeys(3)); + mapper = new DynamoDBMapper(ddbMock, getConfigWithCustomBatchLoadRetryStrategy(new DynamoDBMapperConfig.NoRetryBatchLoadRetryStrategy())); + + assertBatchLoadFails(); + verify(ddbMock, times(1)).batchGetItem(any(BatchGetItemRequest.class)); + } + + private void assertBatchLoadFails() { + try { + mapper.batchLoad(itemsToGet); + fail("Expected BatchGetItemException"); + } catch (BatchGetItemException expected) { + // expected + } + } + + @Test + public void testNoDelayOnPartialFailure_DefaultRetry() { + BatchLoadRetryStrategy defaultRetryStrategy = new DynamoDBMapperConfig.DefaultBatchLoadRetryStrategy(); + BatchGetItemRequest itemRequest = BatchGetItemRequest.builder() + .requestItems(buildUnprocessedKeysMap(3)) + .build(); + BatchGetItemResponse itemResult = BatchGetItemResponse.builder() + .unprocessedKeys(buildUnprocessedKeysMap(2)) + .build(); + BatchLoadContext context = new BatchLoadContext(itemRequest); + context.setBatchGetItemResult(itemResult); + context.setRetriesAttempted(2); + assertEquals(0, defaultRetryStrategy.getDelayBeforeNextRetry(context)); + } + + @Test + public void testDelayOnPartialFailure_DefaultRetry() { + BatchLoadRetryStrategy defaultRetryStrategy = new DynamoDBMapperConfig.DefaultBatchLoadRetryStrategy(); + BatchGetItemRequest itemRequest = BatchGetItemRequest.builder() + .requestItems(buildUnprocessedKeysMap(3)) + .build(); + BatchGetItemResponse itemResult = BatchGetItemResponse.builder() + .unprocessedKeys(buildUnprocessedKeysMap(3)) + .build(); + BatchLoadContext context = new BatchLoadContext(itemRequest); + context.setBatchGetItemResult(itemResult); + context.setRetriesAttempted(2); + assertTrue(defaultRetryStrategy.getDelayBeforeNextRetry(context) > 0); + } + + @Test + public void testBatchLoad_splitsAtHundredKeyBoundary() { + when(ddbMock.batchGetItem(any(BatchGetItemRequest.class))).thenReturn(buildDefaultGetItemResult()); + mapper = new DynamoDBMapper(ddbMock); + + List manyItems = new ArrayList(); + for (int i = 0; i < 101; i++) { + manyItems.add(new Item("hash" + i)); + } + + mapper.batchLoad(manyItems); + + ArgumentCaptor captor = ArgumentCaptor.forClass(BatchGetItemRequest.class); + verify(ddbMock, times(2)).batchGetItem(captor.capture()); + + List requests = captor.getAllValues(); + KeysAndAttributes firstChunk = requests.get(0).requestItems().get(TABLE_NAME); + KeysAndAttributes secondChunk = requests.get(1).requestItems().get(TABLE_NAME); + + assertEquals("first chunk fills the 100-key boundary", 100, firstChunk.keys().size()); + assertEquals("second chunk carries the remaining key", 1, secondChunk.keys().size()); + } + + private DynamoDBMapperConfig getConfigWithCustomBatchLoadRetryStrategy(final BatchLoadRetryStrategy batchReadRetryStrategy) { + return new DynamoDBMapperConfig.Builder().withBatchLoadRetryStrategy(batchReadRetryStrategy).build(); + } + + private Map buildUnprocessedKeysMap(final int size) { + final Map unproccessedKeys = new HashMap(size); + for (int i = 0; i < size; i++) { + unproccessedKeys.put("test" + i, KeysAndAttributes.builder().build()); + } + + return unproccessedKeys; + } + + private BatchGetItemResponse buildDefaultGetItemResult() { + + final Map>> map = new HashMap>>(); + return BatchGetItemResponse.builder().responses(map).build(); + + } + + private BatchGetItemResponse buildGetItemResultWithUnprocessedKeys(final int size) { + return buildDefaultGetItemResult() + .toBuilder() + .unprocessedKeys(buildUnprocessedKeysMap(size)) + .build(); + } + + static class BatchLoadRetryStrategyWithNoDelay implements BatchLoadRetryStrategy { + + private final int maxRetry; + + /** + * @param maxRetry + */ + public BatchLoadRetryStrategyWithNoDelay(final int maxRetry) { + this.maxRetry = maxRetry; + } + + /* (non-Javadoc) + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.BatchLoadRetryStrategy#getMaxRetryOnUnprocessedKeys(java.util.Map, java.util.Map) + */ + @Override + public boolean shouldRetry(final BatchLoadContext batchLoadContext) { + return batchLoadContext.getRetriesAttempted() < maxRetry; + } + + /* (non-Javadoc) + * @see software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.BatchLoadRetryStrategy#getDelayBeforeNextRetry(java.util.Map, int) + */ + @Override + public long getDelayBeforeNextRetry(final BatchLoadContext batchLoadContext) { + return 0; + } + + + + } + + @DynamoDBTable(tableName = TABLE_NAME) + public static class Item { + + private String hash; + + public Item(final String hash) { + this.hash = hash; + } + + @DynamoDBAttribute(attributeName = HASH_ATTR) + @DynamoDBHashKey + public String getHash() { + return hash; + } + + public void setHash(final String hash) { + this.hash = hash; + } + + public WriteRequest toPutSaveRequest() { + return WriteRequest.builder() + .putRequest(PutRequest.builder() + .item(Collections.singletonMap(HASH_ATTR, AttributeValue.builder().s(hash).build())) + .build()) + .build(); + } + } + + @DynamoDBTable(tableName = TABLE_NAME2) + public static class Item2 { + + private String hash; + + public Item2(final String hash) { + this.hash = hash; + } + + @DynamoDBAttribute(attributeName = HASH_ATTR) + @DynamoDBHashKey + public String getHash() { + return hash; + } + + public void setHash(final String hash) { + this.hash = hash; + } + + public WriteRequest toPutSaveRequest() { + return WriteRequest.builder() + .putRequest(PutRequest.builder() + .item(Collections.singletonMap(HASH_ATTR, AttributeValue.builder().s(hash).build())) + .build()) + .build(); + } + } + + @DynamoDBTable(tableName = TABLE_NAME3) + public static class Item3 { + + private String hash; + + public Item3(final String hash) { + this.hash = hash; + } + + @DynamoDBAttribute(attributeName = HASH_ATTR) + @DynamoDBHashKey + public String getHash() { + return hash; + } + + public void setHash(final String hash) { + this.hash = hash; + } + + public WriteRequest toPutSaveRequest() { + return WriteRequest.builder() + .putRequest(PutRequest.builder() + .item(Collections.singletonMap(HASH_ATTR, AttributeValue.builder().s(hash).build())) + .build()) + .build(); + } + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/BatchLoadTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/BatchLoadTest.java new file mode 100644 index 000000000000..65b270f5e4b0 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/BatchLoadTest.java @@ -0,0 +1,234 @@ +/* + * 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.mapper.dynamodb; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.mapper.dynamodb.LocalDynamoDBTestBase; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.ConsistentReads; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.SaveBehavior; +import software.amazon.awssdk.mapper.dynamodb.mapper.NumberSetAttributeClass; +import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest; +import software.amazon.awssdk.mapper.dynamodb.pojos.RangeKeyClass; +import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition; +import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement; +import software.amazon.awssdk.services.dynamodb.model.KeyType; +import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.junit.BeforeClass; +import org.junit.Test; + +public class BatchLoadTest extends LocalDynamoDBTestBase { + // We don't start with the current system millis like other tests because + // it's out of the range of some data types + private static int start = 1; + private static int byteStart = 1; + private static int startKeyDebug = 1; + private static long startKey = System.currentTimeMillis(); + private static DynamoDbClient dynamo; + private static DynamoDBMapper mapper; + private static String tableName; + private static String tableName2; + + @BeforeClass + public static void setUp() throws Exception { + dynamo = client(); + mapper = new DynamoDBMapper(dynamo, new DynamoDBMapperConfig(SaveBehavior.UPDATE, + ConsistentReads.CONSISTENT, null)); + // Table setup is built directly against the v2 client rather than via + // mapper.generateCreateTableRequest(), which is part of the deferred table-admin port. + // The behavior under test is the data plane (batchSave/batchLoad), not table creation. + tableName = "aws-java-sdk-util"; + tableName2 = "aws-java-sdk-range-test"; + dynamo.createTable(hashKeyTable(tableName, "key")); + dynamo.createTable(hashAndRangeKeyTable(tableName2, "key", "rangeKey")); + } + + private static CreateTableRequest hashKeyTable(String tableName, String hashKey) { + return CreateTableRequest.builder() + .tableName(tableName) + .keySchema(KeySchemaElement.builder().attributeName(hashKey).keyType(KeyType.HASH).build()) + .attributeDefinitions(AttributeDefinition.builder().attributeName(hashKey) + .attributeType(ScalarAttributeType.S).build()) + .provisionedThroughput(DEFAULT_PROVISIONED_THROUGHPUT) + .build(); + } + + private static CreateTableRequest hashAndRangeKeyTable(String tableName, String hashKey, String rangeKey) { + return CreateTableRequest.builder() + .tableName(tableName) + .keySchema(KeySchemaElement.builder().attributeName(hashKey).keyType(KeyType.HASH).build(), + KeySchemaElement.builder().attributeName(rangeKey).keyType(KeyType.RANGE).build()) + .attributeDefinitions(AttributeDefinition.builder().attributeName(hashKey) + .attributeType(ScalarAttributeType.N).build(), + AttributeDefinition.builder().attributeName(rangeKey) + .attributeType(ScalarAttributeType.N).build()) + .provisionedThroughput(DEFAULT_PROVISIONED_THROUGHPUT) + .build(); + } + + @Test + public void testBatchLoad() throws InterruptedException { + // To see whether batchGet can handle more than 100 items per request + final int numItems = 200; + List objs = new ArrayList(); + List keyPairs = new LinkedList(); + Class clazz = null; + for ( int i = 0; i < numItems; i++ ) { + NumberSetAttributeClass obj = getUniqueNumericObject(); + objs.add(obj); + clazz = obj.getClass(); + keyPairs.add(new KeyPair().withHashKey(obj.getKey())); + } + + mapper.batchSave(objs); + + Map, List> itemsToGet = new HashMap, List>(); + Map> response = null; + itemsToGet.put(clazz, keyPairs); + response = mapper.batchLoad(itemsToGet); + List items = response.get(tableName); + assertEquals(numItems, items.size()); + + for (Object item : items) { + assertTrue(objs.contains(item)); + } + } + + @Test + public void testMultipleTables() { + final int numItems = 55; + Map, List> itemsToGet = new HashMap, List>(); + Class clazz = null; + List keyPairs = new LinkedList(); + List objs = new ArrayList(); + for ( int i = 0; i < numItems * 2; i++ ) { + NumberSetAttributeClass obj = getUniqueNumericObject(); + clazz = obj.getClass(); + keyPairs.add(new KeyPair().withHashKey(obj.getKey())); + objs.add(obj); + } + itemsToGet.put(clazz, keyPairs); + keyPairs = new LinkedList(); + for ( int i = 0; i < numItems; i++ ) { + RangeKeyClass obj = getUniqueRangeKeyObject(); + clazz = obj.getClass(); + keyPairs.add(new KeyPair().withHashKey(obj.getKey()).withRangeKey(obj.getRangeKey())); + objs.add(obj); + } + itemsToGet.put(clazz, keyPairs); + Collections.shuffle(objs); + + mapper.batchSave(objs); + + Map> response = null; + itemsToGet.put(clazz, keyPairs); + response = mapper.batchLoad(itemsToGet); + + List itemsFromTableOne = response.get(tableName); + List itemsFromTableTwo = response.get(tableName2); + + assertEquals(numItems * 2, itemsFromTableOne.size()); + assertEquals(numItems, itemsFromTableTwo.size()); + + for (Object item : itemsFromTableOne) { + assertTrue(objs.contains(item)); + } + + for (Object item : itemsFromTableTwo) { + assertTrue(objs.contains(item)); + } + } + + @Test + public void testBoudaryCases() { + // The request is an empty Map. + Map, List> itemsToGet = new HashMap, List>(); + Map> response = null; + response = mapper.batchLoad(itemsToGet); + assertTrue(response.isEmpty()); + + // The request only contains invalid key pairs + List keyPairs = new LinkedList(); + Class clazz = getUniqueNumericObject().getClass(); + keyPairs.add(new KeyPair().withHashKey("non-existant-key")); + itemsToGet.clear(); + itemsToGet.put(clazz, keyPairs); + response = mapper.batchLoad(itemsToGet); + assertNotNull(response); + List items = response.get(tableName); + assertNotNull(items); + assertEquals(0, items.size()); + + // The request does not contain any key pairs. + itemsToGet.put(clazz, new LinkedList()); + response = mapper.batchLoad(itemsToGet); + assertTrue(response.isEmpty()); + } + + private NumberSetAttributeClass getUniqueNumericObject() { + NumberSetAttributeClass obj = new NumberSetAttributeClass(); + obj.setKey(String.valueOf(startKeyDebug++)); + obj.setBigDecimalAttribute(toSet(new BigDecimal(startKey++), new BigDecimal(startKey++), new BigDecimal(startKey++))); + obj.setBigIntegerAttribute(toSet(new BigInteger("" + startKey++), new BigInteger("" + startKey++), new BigInteger("" + startKey++))); + obj.setByteObjectAttribute(toSet(new Byte(nextByte()), new Byte(nextByte()), new Byte(nextByte()))); + obj.setDoubleObjectAttribute(toSet(new Double("" + start++), new Double("" + start++), new Double("" + start++))); + obj.setFloatObjectAttribute(toSet(new Float("" + start++), new Float("" + start++), new Float("" + start++))); + obj.setIntegerAttribute(toSet(new Integer("" + start++), new Integer("" + start++), new Integer("" + start++))); + obj.setLongObjectAttribute(toSet(new Long("" + start++), new Long("" + start++), new Long("" + start++))); + obj.setBooleanAttribute(toSet(true, false)); + obj.setDateAttribute(toSet(new Date(startKey++), new Date(startKey++), new Date(startKey++))); + Set cals = new HashSet(); + for ( Date d : obj.getDateAttribute() ) { + Calendar cal = GregorianCalendar.getInstance(); + cal.setTime(d); + cals.add(cal); + } + obj.setCalendarAttribute(toSet(cals)); + return obj; + } + + private RangeKeyClass getUniqueRangeKeyObject() { + RangeKeyClass obj = new RangeKeyClass(); + obj.setKey(startKey++); + obj.setIntegerAttribute(toSet(start++, start++, start++)); + obj.setBigDecimalAttribute(new BigDecimal(startKey++)); + obj.setRangeKey(start++); + obj.setStringAttribute("" + startKey++); + obj.setStringSetAttribute(toSet("" + startKey++, "" + startKey++, "" + startKey++)); + return obj; + } + + private String nextByte() { + return "" + byteStart++ % Byte.MAX_VALUE; + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/BatchWriteRetryStrategyTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/BatchWriteRetryStrategyTest.java new file mode 100644 index 000000000000..fec1084a4c24 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/BatchWriteRetryStrategyTest.java @@ -0,0 +1,215 @@ +/* + * 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.mapper.dynamodb; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.junit.Before; +import org.junit.Test; + +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper.FailedBatch; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.BatchWriteRetryStrategy; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.BatchWriteItemRequest; +import software.amazon.awssdk.services.dynamodb.model.BatchWriteItemResponse; +import software.amazon.awssdk.services.dynamodb.model.PutRequest; +import software.amazon.awssdk.services.dynamodb.model.WriteRequest; + +public class BatchWriteRetryStrategyTest { + + private static final int MAX_RETRY = 10; + private static final String TABLE_NAME = "tableName"; + private static final String HASH_ATTR = "hash"; + + private static Map> unprocessedItems; + + private DynamoDbClient ddbMock; + private DynamoDBMapper mapper; + + static { + WriteRequest writeReq = WriteRequest.builder() + .putRequest(PutRequest.builder() + .item(Collections.singletonMap( + HASH_ATTR, + AttributeValue.builder().s("foo").build())) + .build()) + .build(); + + unprocessedItems = Collections.singletonMap(TABLE_NAME, + Arrays.asList(writeReq)); + } + + @Before + public void setup() { + ddbMock = mock(DynamoDbClient.class); + mapper = new DynamoDBMapper( + ddbMock, + getConfigWithCustomBatchWriteRetryStrategy( + new BatchWriteRetryStrategyWithNoDelay(MAX_RETRY))); + } + + @Test + public void testBatchWriteItemCallSuccess_NoRetry() { + + stubBatchWriteItemSuccess(); + + List failedBatches = mapper.batchSave(new Item("foo")); + + // BatchWriteItem is expected to be called only once + verify(ddbMock, times(1)).batchWriteItem(isA(BatchWriteItemRequest.class)); + + assertEquals(0, failedBatches.size()); + } + + @Test + public void testUnprocessedItemReturned_BatchWriteItemCallNotExceedMaxRetry() { + + stubBatchWriteItemReturnUnprocessedItems(); + + List failedBatches = mapper.batchSave(new Item("foo")); + + // BatchWriteItem is expected to be called exactly (MAX_RETRY + 1) times + verify(ddbMock, times(MAX_RETRY + 1)).batchWriteItem(isA(BatchWriteItemRequest.class)); + + assertEquals(1, failedBatches.size()); + FailedBatch failedBatch = failedBatches.get(0); + + assertEquals( + "Failed batch should contain the same UnprocessedItems returned in the BatchWriteItem response.", + unprocessedItems, + failedBatch.getUnprocessedItems()); + assertNull( + "No exception should be set if the batch failed after max retry", + failedBatch.getException()); + } + + @Test + public void testExceptionThrown_NoRetry() { + + RuntimeException exception = new RuntimeException("BOOM"); + stubBatchWriteItemThrowException(exception); + + // put a random item + Item item = new Item(UUID.randomUUID().toString()); + List failedBatches = mapper.batchSave(item); + + verify(ddbMock, times(1)).batchWriteItem(isA(BatchWriteItemRequest.class)); + + assertEquals(1, failedBatches.size()); + FailedBatch failedBatch = failedBatches.get(0); + + assertEquals( + "Failed batch should contain all the input items for batchWrite", + Collections.singletonMap(TABLE_NAME, Arrays.asList(item.toPutSaveRequest())), + failedBatch.getUnprocessedItems()); + assertSame( + "The exception should be the same as one thrown by BatchWriteItem", + exception, + failedBatch.getException()); + } + + private void stubBatchWriteItemSuccess() { + when(ddbMock.batchWriteItem(isA(BatchWriteItemRequest.class))) + .thenReturn(BatchWriteItemResponse.builder() + .unprocessedItems(Collections.>emptyMap()) + .build()); + } + + private void stubBatchWriteItemReturnUnprocessedItems() { + when(ddbMock.batchWriteItem(isA(BatchWriteItemRequest.class))) + .thenReturn(BatchWriteItemResponse.builder() + .unprocessedItems(unprocessedItems) + .build()); + } + + private void stubBatchWriteItemThrowException(RuntimeException e) { + when(ddbMock.batchWriteItem(isA(BatchWriteItemRequest.class))) + .thenThrow(e); + } + + private DynamoDBMapperConfig getConfigWithCustomBatchWriteRetryStrategy( + BatchWriteRetryStrategy batchWriteRetryStrategy) { + return new DynamoDBMapperConfig.Builder() + .withBatchWriteRetryStrategy(batchWriteRetryStrategy) + .build(); + } + + private static class BatchWriteRetryStrategyWithNoDelay implements + BatchWriteRetryStrategy { + + private final int maxRetry; + + public BatchWriteRetryStrategyWithNoDelay(int maxRety) { + this.maxRetry = maxRety; + } + + @Override + public int getMaxRetryOnUnprocessedItems( + Map> batchWriteItemInput) { + return maxRetry; + } + + @Override + public long getDelayBeforeRetryUnprocessedItems( + Map> unprocessedItems, + int retriesAttempted) { + return 0; + } + + } + + @DynamoDBTable(tableName = TABLE_NAME) + public static class Item { + + private String hash; + + public Item(String hash) { + this.hash = hash; + } + + @DynamoDBHashKey + @DynamoDBAttribute(attributeName = HASH_ATTR) + public String getHash() { + return hash; + } + public void setHash(String hash) { + this.hash = hash; + } + + public WriteRequest toPutSaveRequest() { + return WriteRequest.builder() + .putRequest(PutRequest.builder() + .item(Collections.singletonMap(HASH_ATTR, AttributeValue.builder().s(hash).build())) + .build()) + .build(); + } + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/CachingMarshallerSetTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/CachingMarshallerSetTest.java new file mode 100644 index 000000000000..2e2a0ebf9cdc --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/CachingMarshallerSetTest.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.mapper.dynamodb; + +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.ArrayDeque; +import java.util.Deque; + +import org.junit.Assert; +import org.junit.Test; + +import software.amazon.awssdk.mapper.dynamodb.ConversionSchemas.CachingMarshallerSet; +import software.amazon.awssdk.mapper.dynamodb.ConversionSchemas.MarshallerSet; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.mapper.dynamodb.pojos.TestClass; + +public class CachingMarshallerSetTest { + + private static class TestMarshallerSet implements MarshallerSet { + + private final Deque queue = + new ArrayDeque(); + + private final Deque memberQueue = + new ArrayDeque(); + + @Override + public ArgumentMarshaller getMarshaller(Method getter) { + return queue.remove(); + } + + @Override + public ArgumentMarshaller getMemberMarshaller(Type memberType) { + return memberQueue.remove(); + } + } + + private static final TestMarshallerSet mock = new TestMarshallerSet(); + private static final MarshallerSet sut = new CachingMarshallerSet(mock); + + @Test + public void testIt() throws Exception { + ArgumentMarshaller marshaller = new ArgumentMarshaller() { + @Override + public AttributeValue marshall(Object value) { + return null; + } + }; + + mock.queue.add(marshaller); + + ArgumentMarshaller result = sut.getMarshaller( + TestClass.class.getMethod("getString")); + + Assert.assertSame(marshaller, result); + + result = sut.getMarshaller(TestClass.class.getMethod("getString")); + + Assert.assertSame(marshaller, result); + + ArgumentMarshaller marshaller2 = new ArgumentMarshaller() { + @Override + public AttributeValue marshall(Object value) { + return null; + } + }; + + mock.queue.add(marshaller2); + + result = sut.getMarshaller(TestClass.class.getMethod("getInt")); + + Assert.assertSame(marshaller2, result); + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/CachingUnmarshallerSetTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/CachingUnmarshallerSetTest.java new file mode 100644 index 000000000000..dc4506675213 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/CachingUnmarshallerSetTest.java @@ -0,0 +1,101 @@ +/* + * 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.mapper.dynamodb; + +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.ArrayDeque; +import java.util.Deque; + +import org.junit.Assert; +import org.junit.Test; + +import software.amazon.awssdk.mapper.dynamodb.ConversionSchemas.CachingUnmarshallerSet; +import software.amazon.awssdk.mapper.dynamodb.ConversionSchemas.UnmarshallerSet; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.mapper.dynamodb.pojos.TestClass; + +public class CachingUnmarshallerSetTest { + + private static class TestUnmarshallerSet implements UnmarshallerSet { + + private final Deque queue = + new ArrayDeque(); + + private final Deque memberQueue = + new ArrayDeque(); + + @Override + public ArgumentUnmarshaller getUnmarshaller(Method getter, Method setter) { + return queue.remove(); + } + + @Override + public ArgumentUnmarshaller getMemberUnmarshaller(Type type) { + return memberQueue.remove(); + } + } + + private static final TestUnmarshallerSet mock = new TestUnmarshallerSet(); + private static final UnmarshallerSet sut = new CachingUnmarshallerSet(mock); + + @Test + public void testIt() throws Exception { + ArgumentUnmarshaller unmarshaller = new ArgumentUnmarshaller() { + @Override + public void typeCheck(AttributeValue value, Method setter) { + } + + @Override + public Object unmarshall(AttributeValue value) { + return null; + } + }; + + mock.queue.add(unmarshaller); + + ArgumentUnmarshaller result = sut.getUnmarshaller( + TestClass.class.getMethod("getString"), + TestClass.class.getMethod("setString", String.class)); + + Assert.assertSame(unmarshaller, result); + + result = sut.getUnmarshaller( + TestClass.class.getMethod("getString"), + TestClass.class.getMethod("setString", String.class)); + + Assert.assertSame(unmarshaller, result); + + ArgumentUnmarshaller unmarshaller2 = new ArgumentUnmarshaller() { + @Override + public void typeCheck(AttributeValue value, Method setter) { + } + + @Override + public Object unmarshall(AttributeValue value) { + return null; + } + }; + + mock.queue.add(unmarshaller2); + + result = sut.getUnmarshaller( + TestClass.class.getMethod("getInt"), + TestClass.class.getMethod("setInt", int.class)); + + Assert.assertSame(unmarshaller2, result); + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/ConfigureS3LinksTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/ConfigureS3LinksTest.java new file mode 100644 index 000000000000..aa8d85041cc9 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/ConfigureS3LinksTest.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.mapper.dynamodb; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +import org.junit.Before; +import org.junit.Test; + +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.model.Region; + +public class ConfigureS3LinksTest { + + private S3ClientCache s3cc; + + @Before + public void setUp() throws Exception { + s3cc = new S3ClientCache(new BasicAWSCredentials("mock", "mock")); + } + + @DynamoDBTable(tableName="nonexisting-test-tablename") + public static class CorrectTestClass { + + private String hk; + private S3Link s3; + + public CorrectTestClass() {} + + @DynamoDBHashKey + public String getHk() { return hk; } + public void setHk(String hk) { this.hk = hk; } + + public S3Link getS3() { return s3; } + public void setS3(S3Link s3) { this.s3 = s3; } + } + + @Test + public void testS3LinkWithStringRegion() { + CorrectTestClass obj = new CorrectTestClass(); + S3Link s3 = new S3Link(s3cc, "ap-southeast-1", "nonexisting-test-bucketname2", "key"); + obj.setS3(s3); + + assertNotNull(obj.getS3()); + assertEquals("nonexisting-test-bucketname2", obj.getS3().getBucketName()); + assertSame(Region.AP_Singapore, obj.getS3().getS3Region()); + assertSame("ap-southeast-1", obj.getS3().getRegion()); + } + + @DynamoDBTable(tableName="nonexisting-test-tablename") + public static class ManyS3LinksTestClass { + + private String hk; + private S3Link s3_1; + private S3Link s3_2; + private S3Link s3_3; + private S3Link s3_4; + private S3Link s3_5; + private S3Link s3_6; + + public ManyS3LinksTestClass() {} + + @DynamoDBHashKey + public String getHk() { return hk; } + public void setHk(String hk) { this.hk = hk; } + + public S3Link getS3_1() { return s3_1; } + public void setS3_1(S3Link s3_1) { this.s3_1 = s3_1; } + + public S3Link getS3_2() { return s3_2; } + public void setS3_2(S3Link s3_2) { this.s3_2 = s3_2; } + + public S3Link getS3_3() { return s3_3; } + public void setS3_3(S3Link s3_3) { this.s3_3 = s3_3; } + + public S3Link getS3_4() { return s3_4; } + public void setS3_4(S3Link s3_4) { this.s3_4 = s3_4; } + + public S3Link getS3_5() { return s3_5; } + public void setS3_5(S3Link s3_5) { this.s3_5 = s3_5; } + + public S3Link getS3_6() { return s3_6; } + public void setS3_6(S3Link s3_6) { this.s3_6 = s3_6; } + } + + @Test + public void testManyS3LinksClass() { + ManyS3LinksTestClass obj = new ManyS3LinksTestClass(); + assertNull(obj.getS3_1()); + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/ConversionToAttributeValuesTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/ConversionToAttributeValuesTest.java new file mode 100644 index 000000000000..56eb4a481335 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/ConversionToAttributeValuesTest.java @@ -0,0 +1,190 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.mapper.dynamodb.internal.DynamoDBMapperModelFactory; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import org.junit.Before; +import org.junit.Test; + +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +public class ConversionToAttributeValuesTest { + + private DynamoDBMapperModelFactory models; + private DynamoDBMapperConfig finalConfig; + + @Before + public void setUp() throws Exception { + finalConfig = new DynamoDBMapperConfig.Builder() + .withTypeConverterFactory(DynamoDBMapperConfig.DEFAULT.getTypeConverterFactory()) + .withConversionSchema(ConversionSchemas.V2) + .build(); + this.models = StandardModelFactories.of(S3Link.Factory.of(null)); + } + + @Test + public void converterFailsForSubProperty() throws Exception { + DynamoDBMapperTableModel tableModel = getTable(ConverterData.class); + Map withSubData = tableModel.convert(new ConverterData()); + assertEquals("bar", tableModel.unconvert(withSubData).getSubDocument().getaData().getValue()); + } + + + private DynamoDBMapperTableModel getTable(Class clazz) { + return this.models.getTableFactory(finalConfig).getTable(clazz); + } + + @DynamoDBTable(tableName = "test") + public static class ConverterData { + + @DynamoDBHashKey + private String key; + + @DynamoDBTypeConverted(converter = CustomDataConverter.class) + CustomData customConverted; + + private ConverterSubDocument subDocument; + + public ConverterData() { + customConverted = new CustomData("foo"); + subDocument = new ConverterSubDocument(); + subDocument.setaData(new CustomData("bar")); + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public ConverterSubDocument getSubDocument() { + return subDocument; + } + + public void setSubDocument(ConverterSubDocument subProperty) { + this.subDocument = subProperty; + } + + public CustomData getCustomConverted() { + return customConverted; + } + + public void setCustomConverted(CustomData customConverted) { + this.customConverted = customConverted; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ConverterData that = (ConverterData) o; + return ConversionToAttributeValuesTest.equals(subDocument, that.subDocument); + } + + @Override + public int hashCode() { + return ConversionToAttributeValuesTest.hash(subDocument); + } + + } + + @DynamoDBDocument + public static class ConverterSubDocument { + + @DynamoDBTypeConverted(converter = CustomDataConverter.class) + private CustomData aData; + + public CustomData getaData() { + return aData; + } + + public void setaData(CustomData aData) { + this.aData = aData; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ConverterSubDocument that = (ConverterSubDocument) o; + return ConversionToAttributeValuesTest.equals(aData, that.aData); + } + + @Override + public int hashCode() { + return ConversionToAttributeValuesTest.hash(aData); + } + } + + + public static class CustomData { + + private final String value; + + public CustomData(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CustomData that = (CustomData) o; + return ConversionToAttributeValuesTest.equals(value, that.value); + } + + @Override + public int hashCode() { + return ConversionToAttributeValuesTest.hash(value); + } + } + + public static class CustomDataConverter implements DynamoDBTypeConverter { + + public String convert(CustomData object) { + return object.getValue(); + } + + public CustomData unconvert(String object) { + return new CustomData(object); + } + } + + public static boolean equals(Object o1, Object o2) { + if (o1 == o2) return true; + if (o1 != null) { + return o1.equals(o2); + } + return false; + } + + public static int hash(Object... objs) { + int hash = 7; + for (int i = 0; i < objs.length; ++i) { + hash = hash * 31 + objs[i].hashCode(); + } + return hash; + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBMapperConfigTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBMapperConfigTest.java new file mode 100644 index 000000000000..666550cacd5c --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBMapperConfigTest.java @@ -0,0 +1,46 @@ +/* + * 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.mapper.dynamodb; + +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; + +public class DynamoDBMapperConfigTest { + @Test + public void noCircularStaticInitializationDependencies() { + // This seems silly, but we have to statically initialize these classes by referencing them. If we do this and they + // reference DynamoDBMapperConfig statically, then when we check DynamoDBMapperConfig.DEFAULT's fields, they will be + // null because DynamoDBMapperConfig.DEFAULT would be initialized BEFORE these classes are. + assertNotNull(DynamoDBMapperConfig.SaveBehavior.UPDATE); + assertNotNull(DynamoDBMapperConfig.ConsistentReads.CONSISTENT); + assertNotNull(DynamoDBMapperConfig.PaginationLoadingStrategy.EAGER_LOADING); + assertNotNull(DynamoDBMapperConfig.TableNameResolver.class); + assertNotNull(DynamoDBMapperConfig.BatchWriteRetryStrategy.class); + assertNotNull(DynamoDBMapperConfig.BatchLoadRetryStrategy.class); + assertNotNull(DynamoDBTypeConverterFactory.class); + assertNotNull(ConversionSchema.class); + + assertNotNull(DynamoDBMapperConfig.DEFAULT.getSaveBehavior()); + assertNotNull(DynamoDBMapperConfig.DEFAULT.getConsistentReads()); + assertNotNull(DynamoDBMapperConfig.DEFAULT.getPaginationLoadingStrategy()); + assertNotNull(DynamoDBMapperConfig.DEFAULT.getTableNameResolver()); + assertNotNull(DynamoDBMapperConfig.DEFAULT.getBatchWriteRetryStrategy()); + assertNotNull(DynamoDBMapperConfig.DEFAULT.getBatchLoadRetryStrategy()); + assertNotNull(DynamoDBMapperConfig.DEFAULT.getTypeConverterFactory()); + assertNotNull(DynamoDBMapperConfig.DEFAULT.getConversionSchema()); + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBMapperExpressionsIntegrationTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBMapperExpressionsIntegrationTest.java new file mode 100644 index 000000000000..2f5dbc0cf1cf --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBMapperExpressionsIntegrationTest.java @@ -0,0 +1,345 @@ +/* + * 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.mapper.dynamodb; + +import static org.junit.Assert.assertTrue; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.ComparisonOperator; +import software.amazon.awssdk.services.dynamodb.model.Condition; +import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException; +import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest; +import software.amazon.awssdk.services.dynamodb.model.DescribeTableResponse; +import software.amazon.awssdk.services.dynamodb.model.ExpectedAttributeValue; +import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement; +import software.amazon.awssdk.services.dynamodb.model.KeyType; +import software.amazon.awssdk.services.dynamodb.model.ProvisionedThroughput; +import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; +import software.amazon.awssdk.services.dynamodb.model.ResourceInUseException; +import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType; +import software.amazon.awssdk.mapper.dynamodb.test.AWSTestBase; +import software.amazon.awssdk.mapper.dynamodb.test.util.DynamoDBTestBase; + +public class DynamoDBMapperExpressionsIntegrationTest extends AWSTestBase { + + /** Table name to be used for this testing */ + static final String TABLENAME = "java-sdk-mapper-customer"; + + /** Attribute name of the hash key */ + private static final String HASH_KEY = "customerId"; + + /** Attribute name of the range key */ + private static final String RANGE_KEY = "addressType"; + + /** Status of the table */ + private static final String TABLE_STATUS_ACTIVE = "ACTIVE"; + + /** Sleep time in milli seconds for the table to become active. */ + private static final long SLEEP_TIME_IN_MILLIS = 5000; + + /** Provisioned Throughput read capacity for the table. */ + private static final long READ_CAPACITY = 10; + + /** Provisioned Throughput write capacity for the table. */ + private static final long WRITE_CAPACITY = 10; + + private static final String FIRST_CUSTOMER_ID = "1000"; + private static final String ADDRESS_TYPE_HOME = "home"; + private static final String ADDRESS_TYPE_WORK = "work"; + + /** Reference to the mapper used for this testing */ + protected static DynamoDBMapper mapper; + + /** Reference to the client being used by the mapper. */ + protected static DynamoDbClient client; + + @BeforeClass + public static void setUp() throws FileNotFoundException, IOException, + InterruptedException { + client = DynamoDBTestBase.getClient(); + mapper = new DynamoDBMapper(client); + try { + client.createTable(CreateTableRequest.builder() + .tableName(TABLENAME) + .keySchema(KeySchemaElement.builder().attributeName(HASH_KEY).keyType(KeyType.HASH).build(), + KeySchemaElement.builder().attributeName(RANGE_KEY).keyType(KeyType.RANGE).build()) + .attributeDefinitions( + AttributeDefinition.builder().attributeName(HASH_KEY).attributeType(ScalarAttributeType.N).build(), + AttributeDefinition.builder().attributeName(RANGE_KEY) + .attributeType(ScalarAttributeType.S).build()) + .provisionedThroughput( + ProvisionedThroughput.builder().readCapacityUnits(READ_CAPACITY).writeCapacityUnits(WRITE_CAPACITY).build()) + .build()); + } catch(ResourceInUseException ex) { + ex.printStackTrace(); + } + waitForTableCreation(); + fillInData(); + } + + public static void fillInData() { + final Map record1 = new HashMap(); + record1.put(HASH_KEY, AttributeValue.builder().n(FIRST_CUSTOMER_ID).build()); + record1.put(RANGE_KEY, AttributeValue.builder().s(ADDRESS_TYPE_WORK).build()); + record1.put("AddressLine1", + AttributeValue.builder().s("1918 8th Aven").build()); + record1.put("city", AttributeValue.builder().s("seattle").build()); + record1.put("state", AttributeValue.builder().s("WA").build()); + record1.put("zipcode", AttributeValue.builder().n("98104").build()); + final Map record2 = new HashMap(); + record2.put(HASH_KEY, AttributeValue.builder().n(FIRST_CUSTOMER_ID).build()); + record2.put(RANGE_KEY, AttributeValue.builder().s(ADDRESS_TYPE_HOME).build()); + record2.put("AddressLine1", + AttributeValue.builder().s("15606 NE 40th ST").build()); + record2.put("city", AttributeValue.builder().s("redmond").build()); + record2.put("state", AttributeValue.builder().s("WA").build()); + record2.put("zipcode", AttributeValue.builder().n("98052").build()); + + client.putItem(PutItemRequest.builder().tableName(TABLENAME).item(record1).build()); + client.putItem(PutItemRequest.builder().tableName(TABLENAME).item(record2).build()); + } + + public static void waitForTableCreation() throws InterruptedException { + while (true) { + DescribeTableResponse describeResult = client + .describeTable(b -> b.tableName(TABLENAME)); + if (TABLE_STATUS_ACTIVE.equals(describeResult.table() + .tableStatusAsString())) { + break; + } + Thread.sleep(SLEEP_TIME_IN_MILLIS); + } + } + + /** + * Queries for a record based on hash and range key. Provider a filter + * expression that filters results. + */ + @Test + public void testQueryFilterExpression() { + Customer customer = new Customer(); + customer.setCustomerId(Long.valueOf(FIRST_CUSTOMER_ID)); + + DynamoDBQueryExpression queryExpression = + new DynamoDBQueryExpression() + .withHashKeyValues(customer) + .withRangeKeyCondition(RANGE_KEY, Condition.builder() + .comparisonOperator(ComparisonOperator.EQ) + .attributeValueList(AttributeValue.builder().s(ADDRESS_TYPE_HOME).build()).build()) + ; + PaginatedQueryList results = mapper.query(Customer.class, + queryExpression); + assertTrue(results.size() == 1); + + final Map builder = new HashMap(); + builder.put(":zipcode", AttributeValue.builder().n("98109").build()); + + queryExpression = queryExpression + .withFilterExpression("zipcode = :zipcode") + .withExpressionAttributeValues(builder); + results = mapper.query(Customer.class, queryExpression); + assertTrue(results.size() == 0); + } + + /** + * Queries using key condition expression. + */ + @Test + public void testKeyConditionExpression() { + Customer customer = new Customer(); + customer.setCustomerId(Long.valueOf(FIRST_CUSTOMER_ID)); + + DynamoDBQueryExpression qxp = + new DynamoDBQueryExpression() + .withKeyConditionExpression( + "customerId = :customerId AND addressType = :addressType"); + final Map builder = + new HashMap(); + builder.put(":customerId", AttributeValue.builder().n(FIRST_CUSTOMER_ID).build()); + builder.put(":addressType", AttributeValue.builder().s(ADDRESS_TYPE_HOME).build()); + qxp.withExpressionAttributeValues(builder); + + PaginatedQueryList results = mapper.query(Customer.class, qxp); + assertTrue(results.size() == 1); + + builder.put(":zipcode", AttributeValue.builder().n("98109").build()); + qxp.withFilterExpression("zipcode = :zipcode") + .withExpressionAttributeValues(builder) + ; + + results = mapper.query(Customer.class, qxp); + assertTrue(results.size() == 0); + } + + /** + * Scan the table and filters the results based on the filter expression + * provided. + */ + @Test + public void testScanFilterExpression() { + Customer customer = new Customer(); + customer.setCustomerId(Long.valueOf(FIRST_CUSTOMER_ID)); + + DynamoDBScanExpression scanExpression = new DynamoDBScanExpression(); + + PaginatedScanList results = mapper.scan(Customer.class, + scanExpression); + assertTrue(results.size() == 2); + + final Map attributeValueMapBuilder = new HashMap(); + attributeValueMapBuilder + .put(":state", AttributeValue.builder().s("WA").build()); + + final Map attributeNameMapBuilder = new HashMap(); + attributeNameMapBuilder.put("#statename", "state"); + + scanExpression = scanExpression + .withFilterExpression("#statename = :state") + .withExpressionAttributeValues(attributeValueMapBuilder) + .withExpressionAttributeNames(attributeNameMapBuilder); + results = mapper.scan(Customer.class, scanExpression); + assertTrue(results.size() == 2); + } + + /** + * Performs delete operation with a condition expression specified. Delete + * should fail as the condition in the conditional expression evaluates to + * false. + */ + @Test + public void testDeleteConditionalExpression() { + Customer customer = new Customer(); + customer.setCustomerId(Long.valueOf(FIRST_CUSTOMER_ID)); + customer.setAddressType(ADDRESS_TYPE_WORK); + + Map expectedMapBuilder = new HashMap(); + expectedMapBuilder.put("zipcode", ExpectedAttributeValue.builder() + .attributeValueList(AttributeValue.builder().n("98052").build()) + .comparisonOperator(ComparisonOperator.EQ).build()); + + DynamoDBDeleteExpression deleteExpression = new DynamoDBDeleteExpression(); + deleteExpression.setConditionExpression("zipcode = :zipcode"); + + final Map attributeValueMapBuilder = new HashMap(); + attributeValueMapBuilder.put(":zipcode", + AttributeValue.builder().n("98052").build()); + deleteExpression.setExpressionAttributeValues(attributeValueMapBuilder); + try { + mapper.delete(customer, deleteExpression); + } catch (Exception e) { + assertTrue(e instanceof ConditionalCheckFailedException); + } + } + + @AfterClass + public static void tearDown() { + try { + if (client != null) { + client.deleteTable(b -> b.tableName(TABLENAME)); + } + } catch (Exception e) { + } finally { + if (client != null) + client.close(); + } + } + + // Note don't move Customer to top level, or else it would break the release + // pipeline, as the integration test will not be copied over causing + // compilation failure + @DynamoDBTable(tableName = DynamoDBMapperExpressionsIntegrationTest.TABLENAME) + public static class Customer { + + private long customerId; + + private String addressType; + + private String addressLine1; + + private String city; + + private String state; + + private int zipcode; + + @DynamoDBAttribute(attributeName = "customerId") + @DynamoDBHashKey(attributeName = "customerId") + public long getCustomerId() { + return customerId; + } + + public void setCustomerId(long customerId) { + this.customerId = customerId; + } + + @DynamoDBAttribute(attributeName = "addressType") + @DynamoDBRangeKey(attributeName = "addressType") + public String getAddressType() { + return addressType; + } + + public void setAddressType(String addressType) { + this.addressType = addressType; + } + + @DynamoDBAttribute(attributeName = "AddressLine1") + public String getAddressLine1() { + return addressLine1; + } + + public void setAddressLine1(String addressLine1) { + this.addressLine1 = addressLine1; + } + + @DynamoDBAttribute(attributeName = "city") + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + @DynamoDBAttribute(attributeName = "state") + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + @DynamoDBAttribute(attributeName = "zipcode") + public int getZipcode() { + return zipcode; + } + + public void setZipcode(int zipcode) { + this.zipcode = zipcode; + } + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBMapperIntegrationTestBase.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBMapperIntegrationTestBase.java new file mode 100644 index 000000000000..f06b0d878cf7 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/DynamoDBMapperIntegrationTestBase.java @@ -0,0 +1,44 @@ +/* + * 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.mapper.dynamodb; + +import java.nio.ByteBuffer; +import java.util.HashSet; +import java.util.Set; + +import software.amazon.awssdk.mapper.dynamodb.test.util.DynamoDBIntegrationTestBase; +import software.amazon.awssdk.mapper.dynamodb.test.util.DynamoDBTestBase; +import software.amazon.awssdk.mapper.dynamodb.pojos.BinaryAttributeByteBufferClass; + +public class DynamoDBMapperIntegrationTestBase extends DynamoDBIntegrationTestBase { + + public static void setUpMapperTestBase() { + DynamoDBTestBase.setUpTestBase(); + } + + /* + * Utility methods + */ + protected static BinaryAttributeByteBufferClass getUniqueByteBufferObject(int contentLength) { + BinaryAttributeByteBufferClass obj = new BinaryAttributeByteBufferClass(); + obj.setKey(String.valueOf(startKey++)); + obj.setBinaryAttribute(ByteBuffer.wrap(generateByteArray(contentLength))); + Set byteBufferSet = new HashSet(); + byteBufferSet.add(ByteBuffer.wrap(generateByteArray(contentLength))); + obj.setBinarySetAttribute(byteBufferSet); + return obj; + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/EnumMarshallerIntegrationTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/EnumMarshallerIntegrationTest.java new file mode 100644 index 000000000000..37ef3203bed7 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/EnumMarshallerIntegrationTest.java @@ -0,0 +1,97 @@ +/* + * 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.mapper.dynamodb; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import org.junit.Test; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperIntegrationTestBase; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAutoGeneratedKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMarshalling; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; + +/** + * Status tests for {@code EnumMarshaller}. + */ +public class EnumMarshallerIntegrationTest extends DynamoDBMapperIntegrationTestBase { + + @Test + public void testNullEnumValue() { + final DynamoDBMapper mapper = new DynamoDBMapper(dynamo); + + final TestObject object1 = new TestObject(); + + assertNull(object1.getStatus()); + + mapper.save(object1); + + final TestObject object2 = mapper.load(TestObject.class, object1.getKey()); + + assertNull(object2.getStatus()); + } + + @Test + public void testMarshalling() { + final DynamoDBMapper mapper = new DynamoDBMapper(dynamo); + + final TestObject object1 = new TestObject(); + + object1.setStatus(TestObject.Status.Y); + + mapper.save(object1); + + assertNotNull(object1.getKey()); + assertNotNull(object1.getStatus()); + + final TestObject object2 = mapper.load(TestObject.class, object1.getKey()); + + assertEquals(object1.getKey(), object2.getKey()); + assertEquals(object1.getStatus(), object2.getStatus()); + } + + @DynamoDBTable(tableName="aws-java-sdk-util") + public static class TestObject { + public static class StatusEnumMarshaller extends AbstractEnumMarshaller { }; + public static enum Status { X, Y, Z }; + + private String key; + private Status status; + + @DynamoDBHashKey + @DynamoDBAutoGeneratedKey + public String getKey() { + return this.key; + } + + public void setKey(String key) { + this.key = key; + } + + @DynamoDBMarshalling(marshallerClass=StatusEnumMarshaller.class) + public Status getStatus() { + return this.status; + } + + public void setStatus(Status status) { + this.status = status; + } + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/GenerateDeleteTableRequestTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/GenerateDeleteTableRequestTest.java new file mode 100644 index 000000000000..9e05a2f1195f --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/GenerateDeleteTableRequestTest.java @@ -0,0 +1,58 @@ +/* + * 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.mapper.dynamodb; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import com.amazonaws.services.dynamodbv2.model.DeleteTableRequest; + +/** + * Unit tests for {@link DynamoDBMapper#generateDeleteTableRequest(Class)}. + */ +public class GenerateDeleteTableRequestTest { + + private static final String TABLE_PREFIX = "DEV-"; + private static final String TABLE_NAME = "OBJECTORMEXAMPLE"; + + @Test + public void tableNameNotOverriden_UsesTableNameAttributeInAnnotation() { + DynamoDBMapper dynamoDBMapper = new DynamoDBMapper(null); + DeleteTableRequest deleteTableRequest = dynamoDBMapper.generateDeleteTableRequest(ObjectORMExample.class); + assertEquals(deleteTableRequest.getTableName(), TABLE_NAME); + } + + @Test + public void tableNameOverriddenInConfig_UsesPrefixedOverrideTableName() { + DynamoDBMapperConfig.TableNameOverride tableNameOverride = DynamoDBMapperConfig.TableNameOverride + .withTableNamePrefix(TABLE_PREFIX); + DynamoDBMapperConfig config = new DynamoDBMapperConfig(tableNameOverride); + DynamoDBMapper dynamoDBMapper = new DynamoDBMapper(null, config); + + DeleteTableRequest deleteTableRequest = dynamoDBMapper.generateDeleteTableRequest(ObjectORMExample.class); + assertEquals(deleteTableRequest.getTableName(), TABLE_PREFIX.concat(TABLE_NAME)); + } + + @DynamoDBTable(tableName = TABLE_NAME) + private static class ObjectORMExample { + private String id; + + @DynamoDBHashKey + public final String getId() { return this.id; } + public final void setId(String id) { this.id = id; } + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/GsiAlwaysUpdateTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/GsiAlwaysUpdateTest.java new file mode 100644 index 000000000000..9982172fa0e5 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/GsiAlwaysUpdateTest.java @@ -0,0 +1,98 @@ +/* + * 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.mapper.dynamodb; + +import static org.junit.Assert.assertNotEquals; + +import java.util.UUID; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import software.amazon.awssdk.mapper.dynamodb.pojos.GsiWithAlwaysUpdateTimestamp; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition; +import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest; +import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; +import software.amazon.awssdk.services.dynamodb.model.GlobalSecondaryIndex; +import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement; +import software.amazon.awssdk.services.dynamodb.model.KeyType; +import software.amazon.awssdk.services.dynamodb.model.Projection; +import software.amazon.awssdk.services.dynamodb.model.ProjectionType; +import software.amazon.awssdk.services.dynamodb.model.ProvisionedThroughput; +import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType; + +public class GsiAlwaysUpdateTest extends LocalDynamoDBTestBase { + + private static final String TABLE_NAME = + GsiAlwaysUpdateTest.class.getSimpleName() + "-" + System.currentTimeMillis(); + + private DynamoDbClient ddb; + private DynamoDBTableMapper mapper; + + @Before + public void setup() throws InterruptedException { + ddb = client(); + mapper = new DynamoDBMapper(ddb, DynamoDBMapperConfig.builder() + .withTableNameOverride(new DynamoDBMapperConfig.TableNameOverride(TABLE_NAME)) + .build()).newTableMapper(GsiWithAlwaysUpdateTimestamp.class); + // Table setup is built directly against the v2 client rather than via the mapper's + // createTable(), which is part of the deferred table-admin port. Against DynamoDB Local + // the table is active immediately, so no waiter is needed. The behavior under test is that + // an ALWAYS auto-generated timestamp indexed by a GSI is regenerated on every save. + ProvisionedThroughput throughput = ProvisionedThroughput.builder() + .readCapacityUnits(5L).writeCapacityUnits(5L).build(); + ddb.createTable(CreateTableRequest.builder() + .tableName(TABLE_NAME) + .keySchema(KeySchemaElement.builder().attributeName("hashKey").keyType(KeyType.HASH).build(), + KeySchemaElement.builder().attributeName("rangeKey").keyType(KeyType.RANGE).build()) + .attributeDefinitions( + AttributeDefinition.builder().attributeName("hashKey").attributeType(ScalarAttributeType.S).build(), + AttributeDefinition.builder().attributeName("rangeKey").attributeType(ScalarAttributeType.S).build(), + AttributeDefinition.builder().attributeName("lastModifiedDate") + .attributeType(ScalarAttributeType.N).build()) + .globalSecondaryIndexes(GlobalSecondaryIndex.builder() + .indexName("last-mod-date") + .keySchema(KeySchemaElement.builder().attributeName("lastModifiedDate").keyType(KeyType.HASH).build()) + .projection(Projection.builder().projectionType(ProjectionType.ALL).build()) + .provisionedThroughput(throughput) + .build()) + .provisionedThroughput(throughput) + .build()); + } + + @After + public void tearDown() { + ddb.deleteTable(DeleteTableRequest.builder().tableName(TABLE_NAME).build()); + } + + @Test + public void pojoWithAlwaysGenerateGsi_SavesCorrectly() throws InterruptedException { + final String hashKey = UUID.randomUUID().toString(); + final String rangeKey = UUID.randomUUID().toString(); + + mapper.save(new GsiWithAlwaysUpdateTimestamp() + .setHashKey(hashKey) + .setRangeKey(rangeKey)); + final GsiWithAlwaysUpdateTimestamp created = mapper.load(hashKey, rangeKey); + // Have to store it since the mapper will auto update any generated values in the saved object. + Long createdDate = created.getLastModifiedDate(); + // Need to wait a bit for the timestamps to actually be different + Thread.sleep(1000); + mapper.save(created); + final GsiWithAlwaysUpdateTimestamp updated = mapper.load(hashKey, rangeKey); + assertNotEquals(createdDate, updated.getLastModifiedDate()); + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/JsonMarshallerIntegrationTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/JsonMarshallerIntegrationTest.java new file mode 100644 index 000000000000..1ca15d0cae69 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/JsonMarshallerIntegrationTest.java @@ -0,0 +1,231 @@ +/* + * 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.mapper.dynamodb; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import org.junit.Test; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperIntegrationTestBase; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAutoGeneratedKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMarshalling; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; + +/** + * Status tests for {@code JsonMarshaller}. + */ +public class JsonMarshallerIntegrationTest extends DynamoDBMapperIntegrationTestBase { + + @Test + public void testMarshalling() { + final DynamoDBMapper mapper = new DynamoDBMapper(dynamo); + + final TestObject object1 = new TestObject(); + object1.setOneItem(new TestObject.OneItem()); + object1.addOneItem(new TestObject.OneItem(UUID.randomUUID().toString(), 1)); + object1.addOneItem(new TestObject.OneItem(UUID.randomUUID().toString(), 2)); + object1.addOneItem(new TestObject.OneItem(UUID.randomUUID().toString(), 3)); + object1.setTwoItem(new TestObject.TwoItem()); + object1.addTwoItem(new TestObject.TwoItem(UUID.randomUUID().toString(), new Date())); + + mapper.save(object1); + + assertNotNull(object1.getKey()); + + assertNotNull(object1.getOneItem()); + assertNotNull(object1.getOneItems()); + assertEquals(3, object1.getOneItems().size()); + + assertNotNull(object1.getTwoItem()); + assertNotNull(object1.getTwoItems()); + assertEquals(1, object1.getTwoItems().size()); + + final TestObject object2 = mapper.load(TestObject.class, object1.getKey()); + + assertEquals(object1.getKey(), object2.getKey()); + + assertEquals(object1.getOneItem().getId(), object2.getOneItem().getId()); + assertEquals(object1.getOneItem().getQuantity(), object2.getOneItem().getQuantity()); + assertEquals(object1.getOneItems().size(), object2.getOneItems().size()); + + for (int i = 0, its = object1.getOneItems().size(); i < its; i++) { + assertEquals(object1.getOneItems().get(i).getId(), object2.getOneItems().get(i).getId()); + assertEquals(object1.getOneItems().get(i).getQuantity(), object2.getOneItems().get(i).getQuantity()); + } + + assertEquals(object1.getTwoItem().getId(), object2.getTwoItem().getId()); + assertEquals(object1.getTwoItem().getDate(), object2.getTwoItem().getDate()); + assertEquals(object1.getTwoItems().size(), object2.getTwoItems().size()); + + for (int i = 0, its = object1.getTwoItems().size(); i < its; i++) { + assertEquals(object1.getTwoItems().get(i).getId(), object2.getTwoItems().get(i).getId()); + assertEquals(object1.getTwoItems().get(i).getDate(), object2.getTwoItems().get(i).getDate()); + } + } + + @DynamoDBTable(tableName="aws-java-sdk-util") + public static class TestObject { + public static class OneItemJsonMarshaller extends JsonMarshaller { }; + public static class OneListJsonMarshaller extends JsonMarshaller { + public static final class Type extends ArrayList { }; + public OneListJsonMarshaller() { + super(Type.class); + } + }; + + public static class TwoItemJsonMarshaller extends JsonMarshaller { }; + public static class TwoListJsonMarshaller extends JsonMarshaller { + public static final class Type extends ArrayList { }; + public TwoListJsonMarshaller() { + super(Type.class); + } + }; + + private String key; + private OneItem aitem; + private List oneItems; + private TwoItem bitem; + private List twoItems; + + @DynamoDBHashKey + @DynamoDBAutoGeneratedKey + public String getKey() { + return this.key; + } + + public void setKey(String key) { + this.key = key; + } + + @DynamoDBMarshalling(marshallerClass=OneItemJsonMarshaller.class) + public OneItem getOneItem() { + return this.aitem; + } + + public void setOneItem(OneItem aitem) { + this.aitem = aitem; + } + + @DynamoDBMarshalling(marshallerClass=OneListJsonMarshaller.class) + public List getOneItems() { + return this.oneItems; + } + + public void setOneItems(List oneItems) { + this.oneItems = oneItems; + } + + public void addOneItem(OneItem aitem) { + if (this.oneItems == null) { + this.oneItems = new ArrayList(); + } + this.oneItems.add(aitem); + } + + @DynamoDBMarshalling(marshallerClass=TwoItemJsonMarshaller.class) + public TwoItem getTwoItem() { + return this.bitem; + } + + public void setTwoItem(TwoItem bitem) { + this.bitem = bitem; + } + + @DynamoDBMarshalling(marshallerClass=TwoListJsonMarshaller.class) + public List getTwoItems() { + return this.twoItems; + } + + public void setTwoItems(List twoItems) { + this.twoItems = twoItems; + } + + public void addTwoItem(TwoItem bitem) { + if (this.twoItems == null) { + this.twoItems = new ArrayList(); + } + this.twoItems.add(bitem); + } + + public static class OneItem { + private String id; + private Integer quantity; + + public OneItem(String id, Integer quantity) { + this.id = id; + this.quantity = quantity; + } + + public OneItem() { + this(null, null); + } + + public String getId() { + return this.id; + } + + public void setId(String id) { + this.id = id; + } + + public Integer getQuantity() { + return this.quantity; + } + + public void setQuantity(Integer quantity) { + this.quantity = quantity; + } + } + + public static class TwoItem { + private String id; + private Date date; + + public TwoItem(String id, Date date) { + this.id = id; + this.date = date; + } + + public TwoItem() { + this(null, null); + } + + public String getId() { + return this.id; + } + + public void setId(String id) { + this.id = id; + } + + public Date getDate() { + return this.date; + } + + public void setDate(Date date) { + this.date = date; + } + } + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/LocalDynamoDB.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/LocalDynamoDB.java new file mode 100644 index 000000000000..2e520a1613f8 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/LocalDynamoDB.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.mapper.dynamodb; + +import com.amazonaws.services.dynamodbv2.local.main.ServerRunner; +import com.amazonaws.services.dynamodbv2.local.server.DynamoDBProxyServer; +import java.io.IOException; +import java.net.ServerSocket; +import java.net.URI; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.DynamoDbClientBuilder; + +/** + * Wrapper for a local DynamoDb server used in testing. Each instance of this class will find a new port to run on, + * so multiple instances can be safely run simultaneously. Each instance of this service uses memory as a storage medium + * and is thus completely ephemeral; no data will be persisted between stops and starts. + * + * LocalDynamoDb localDynamoDb = new LocalDynamoDb(); + * localDynamoDb.start(); // Start the service running locally on host + * AmazonDynamoDB dynamoDbClient = localDynamoDb.createClient(); + * ... // Do your testing with the client + * localDynamoDb.stop(); // Stop the service and free up resources + * + * If possible it's recommended to keep a single running instance for all your tests, as it can be slow to teardown + * and create new servers for every test, but there have been observed problems when dropping tables between tests for + * this scenario, so it's best to write your tests to be resilient to tables that already have data in them. + */ +class LocalDynamoDB { + private DynamoDBProxyServer server; + private int port; + + /** + * Start the local DynamoDb service and run in background + */ + void start() { + port = getFreePort(); + String portString = Integer.toString(port); + + try { + server = createServer(portString); + server.start(); + } catch (Exception e) { + throw propagate(e); + } + } + + /** + * Create a standard AWS v2 SDK client pointing to the local DynamoDb instance + * @return A DynamoDbClient pointing to the local DynamoDb instance + */ + DynamoDbClient createClient() { + return clientBuilder().build(); + } + + /** + * Create a client with a caller-supplied override configuration (e.g. a custom retry policy). + */ + DynamoDbClient createClient(ClientOverrideConfiguration overrideConfiguration) { + return clientBuilder().overrideConfiguration(overrideConfiguration).build(); + } + + private DynamoDbClientBuilder clientBuilder() { + String endpoint = String.format("http://localhost:%d", port); + return DynamoDbClient.builder() + .endpointOverride(URI.create(endpoint)) + // The region is meaningless for local DynamoDb but required for client builder validation + .region(Region.US_EAST_1) + .credentialsProvider(StaticCredentialsProvider.create( + AwsBasicCredentials.create("akid", "skid"))); + } + + DynamoDbAsyncClient createAsyncClient() { + String endpoint = String.format("http://localhost:%d", port); + return DynamoDbAsyncClient.builder() + .endpointOverride(URI.create(endpoint)) + .region(Region.US_EAST_1) + .credentialsProvider(StaticCredentialsProvider.create( + AwsBasicCredentials.create("akid", "skid"))) + .build(); + } + + /** + * Stops the local DynamoDb service and frees up resources it is using. + */ + void stop() { + try { + server.stop(); + } catch (Exception e) { + throw propagate(e); + } + } + + private DynamoDBProxyServer createServer(String portString) throws Exception { + return ServerRunner.createServerFromCommandLineArgs( + new String[]{ + "-inMemory", + "-port", portString + }); + } + + private int getFreePort() { + try { + ServerSocket socket = new ServerSocket(0); + int port = socket.getLocalPort(); + socket.close(); + return port; + } catch (IOException ioe) { + throw propagate(ioe); + } + } + + private static RuntimeException propagate(Exception e) { + if (e instanceof RuntimeException) { + throw (RuntimeException)e; + } + throw new RuntimeException(e); + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/LocalDynamoDBTestBase.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/LocalDynamoDBTestBase.java new file mode 100644 index 000000000000..1d5fd13e348b --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/LocalDynamoDBTestBase.java @@ -0,0 +1,102 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.ProvisionedThroughput; +import software.amazon.awssdk.mapper.dynamodb.pojos.BinaryAttributeByteBufferClass; +import java.nio.ByteBuffer; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import org.junit.AfterClass; +import org.junit.BeforeClass; + +public class LocalDynamoDBTestBase { + protected static final ProvisionedThroughput DEFAULT_PROVISIONED_THROUGHPUT = + ProvisionedThroughput.builder().readCapacityUnits(50L).writeCapacityUnits(50L).build(); + private static final LocalDynamoDB LOCAL = new LocalDynamoDB(); + + @BeforeClass + public static void initializeLocalDynamoDb() { + LOCAL.start(); + } + + @AfterClass + public static void stopLocalDynamoDb() { + LOCAL.stop(); + } + + protected static DynamoDbClient client() { + return LOCAL.createClient(); + } + + protected static DynamoDbClient client(ClientOverrideConfiguration configuration) { + return LOCAL.createClient(configuration); + } + + protected static DynamoDbAsyncClient asyncClient() { + return LOCAL.createAsyncClient(); + } + + protected Map getMapKey(String attributeName, AttributeValue value) { + HashMap map = new HashMap(); + map.put(attributeName, value); + return map; + } + + protected static byte[] generateByteArray(int length) { + byte[] bytes = new byte[length]; + + for(int i = 0; i < length; ++i) { + bytes[i] = (byte)(i % 127); + } + + return bytes; + } + + protected static Set toSet(T... array) { + Set set = new HashSet(); + for ( T t : array ) { + set.add(t); + } + return set; + } + + protected static Set toSet(Collection collection) { + Set set = new HashSet(); + for ( T t : collection ) { + set.add(t); + } + return set; + } + + protected static BinaryAttributeByteBufferClass getUniqueByteBufferObject(int contentLength) { + BinaryAttributeByteBufferClass obj = new BinaryAttributeByteBufferClass(); + obj.setKey(UUID.randomUUID().toString()); + obj.setBinaryAttribute(ByteBuffer.wrap(generateByteArray(contentLength))); + Set byteBufferSet = new HashSet(); + byteBufferSet.add(ByteBuffer.wrap(generateByteArray(contentLength))); + obj.setBinarySetAttribute(byteBufferSet); + return obj; + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/MapperBinaryUtilsTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/MapperBinaryUtilsTest.java new file mode 100644 index 000000000000..29ec01d91fc6 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/MapperBinaryUtilsTest.java @@ -0,0 +1,70 @@ +/* + * 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.mapper.dynamodb; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; + +import java.nio.ByteBuffer; + +import org.junit.Test; + +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.mapper.dynamodb.internal.MapperBinaryUtils; + +/** + * Tests {@link MapperBinaryUtils#toWritableByteBuffer(SdkBytes)}, which reproduces the v1 mapper's + * behavior of returning a writable {@link ByteBuffer} for binary attributes. + */ +public class MapperBinaryUtilsTest { + + @Test + public void toWritableByteBuffer_nullInput_returnsNull() { + assertNull(MapperBinaryUtils.toWritableByteBuffer(null)); + } + + @Test + public void toWritableByteBuffer_preservesContents() { + byte[] bytes = {1, 2, 3, 4}; + ByteBuffer result = MapperBinaryUtils.toWritableByteBuffer(SdkBytes.fromByteArray(bytes)); + + byte[] actual = new byte[result.remaining()]; + result.get(actual); + assertArrayEquals(bytes, actual); + } + + @Test + public void toWritableByteBuffer_returnsWritableBuffer() { + ByteBuffer result = MapperBinaryUtils.toWritableByteBuffer(SdkBytes.fromUtf8String("data")); + + assertFalse(result.isReadOnly()); + result.put(0, (byte) 'X'); + assertEquals((byte) 'X', result.get(0)); + } + + @Test + public void toWritableByteBuffer_returnsIndependentCopy() { + SdkBytes source = SdkBytes.fromByteArray(new byte[] {1, 2, 3}); + ByteBuffer result = MapperBinaryUtils.toWritableByteBuffer(source); + + result.put(0, (byte) 99); + + // Mutating the returned buffer must not corrupt the source SdkBytes. + assertArrayEquals(new byte[] {1, 2, 3}, source.asByteArray()); + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/MapperDateUtilsTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/MapperDateUtilsTest.java new file mode 100644 index 000000000000..ed83e2910413 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/MapperDateUtilsTest.java @@ -0,0 +1,142 @@ +/* + * 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.mapper.dynamodb; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.SimpleTimeZone; + +import org.junit.Test; +import software.amazon.awssdk.mapper.dynamodb.internal.MapperDateUtils; + +/** + * Tests ISO-8601 parsing and formatting in {@link MapperDateUtils}, including fractional seconds, + * UTC offsets, and year boundary edge cases. + */ +public class MapperDateUtilsTest { + + @Test + public void formatIso8601Date() throws ParseException { + Date date = new Date(); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); + sdf.setTimeZone(new SimpleTimeZone(0, "GMT")); + String expected = sdf.format(date); + String actual = MapperDateUtils.formatISO8601Date(date); + assertEquals(expected, actual); + + Date expectedDate = sdf.parse(expected); + Date actualDate = MapperDateUtils.parseISO8601Date(actual); + assertEquals(expectedDate, actualDate); + } + + @Test + public void formatISO8601Date_zeroMilliseconds_keepsFractionalSeconds() { + Date date = new Date(0L); + assertEquals("1970-01-01T00:00:00.000Z", MapperDateUtils.formatISO8601Date(date)); + } + + @Test + public void parseIso8601Date_usingAlternativeFormat() throws ParseException { + Date date = new Date(); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + sdf.setTimeZone(new SimpleTimeZone(0, "GMT")); + String formatted = sdf.format(date); + + Date expectedDate = sdf.parse(formatted); + Date actualDate = MapperDateUtils.parseISO8601Date(formatted); + assertEquals(expectedDate, actualDate); + } + + @Test + public void iso8601_withUTCOffset() throws ParseException { + String input = "2021-05-10T17:12:13-07:00"; + Date actualDate = MapperDateUtils.parseISO8601Date(input); + + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX"); + sdf.setTimeZone(new SimpleTimeZone(0, "GMT")); + assertEquals(sdf.parse(input), actualDate); + } + + @Test + public void parseIso8601Date_spotFleetPlusZeroSuffix_isNormalized() throws ParseException { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); + sdf.setTimeZone(new SimpleTimeZone(0, "GMT")); + Date expected = sdf.parse("2014-03-06T14:28:58.123Z"); + assertEquals(expected, MapperDateUtils.parseISO8601Date("2014-03-06T14:28:58.123+0000")); + } + + @Test(expected=IllegalArgumentException.class) + public void invalidDate() throws ParseException { + final String input = "2014-03-06T14:28:58.000Z.000Z"; + MapperDateUtils.parseISO8601Date(input); + } + + @Test + public void testIssue233() throws ParseException { + // https://github.com/aws/aws-sdk-java/issues/233 + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); + sdf.setTimeZone(new SimpleTimeZone(0, "GMT")); + final String edgeCase = "292278994-08-17T07:12:55.807Z"; + Date expected = sdf.parse(edgeCase); + String formatted = MapperDateUtils.formatISO8601Date(expected); + assertEquals(edgeCase, formatted); + Date parsed = MapperDateUtils.parseISO8601Date(edgeCase); + assertEquals(expected, parsed); + String reformatted = MapperDateUtils.formatISO8601Date(parsed); + assertEquals(edgeCase, reformatted); + } + + @Test + public void testIssueDaysDiff() throws ParseException { + // https://github.com/aws/aws-sdk-java/issues/233 + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); + sdf.setTimeZone(new SimpleTimeZone(0, "GMT")); + String edgeCase = "292278994-08-17T07:12:55.807Z"; + String testCase = "292278993-08-17T07:12:55.807Z"; + Date od = sdf.parse(edgeCase); + Date testd = sdf.parse(testCase); + long diff = od.getTime() - testd.getTime(); + assertTrue(diff == 365L*24*60*60*1000); + } + + @Test + public void testIssue233Overflows() throws ParseException { + // 1 milli second passed the max time. Fails for JT <2.9, succeeds on >=2.9. + testOverflow("292278994-08-17T07:12:55.808Z", true); + + // 1 year passed the max year + testOverflow("292278995-01-17T07:12:55.807Z", false); + } + + private void testOverflow(String edgeCase, boolean successExpected) { + try { + Date parsed = MapperDateUtils.parseISO8601Date(edgeCase); + if (!successExpected) { + // We should have failed! + fail("Unexpected success: " + edgeCase + " --> " + parsed); + } + } catch (IllegalArgumentException ex) { + if (successExpected) { + fail("Unexpected failure: " + edgeCase); + } + } + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/PaginatedScanTaskTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/PaginatedScanTaskTest.java new file mode 100644 index 000000000000..ef76f356a926 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/PaginatedScanTaskTest.java @@ -0,0 +1,202 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.ProvisionedThroughputExceededException; +import software.amazon.awssdk.services.dynamodb.model.ScanRequest; +import software.amazon.awssdk.services.dynamodb.model.ScanResponse; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatcher; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class PaginatedScanTaskTest { + + private static final String TABLE_NAME = "FooTable"; + + private static final int TOTAL_SEGMENTS = 5; + + private ParallelScanTask parallelScanTask; + + private ExecutorService executorService; + + @Mock + private DynamoDbClient dynamoDB; + + @Before + public void setup() { + executorService = Executors.newSingleThreadExecutor(); + parallelScanTask = new ParallelScanTask(dynamoDB, createScanRequests(), executorService); + } + + /** + * A failed segment makes the scan task unusable and will always rethrow the same exception. In + * this case it makes sense to shutdown the executor so that applications can shutdown faster. A + * future enhancement could be to either retry failed segments, explicitly resume a failed scan, + * or include metadata in the thrown exception about the state of the scan at the time it was + * aborted. See PR #624 and Issue #624 for more details. + */ + @Test + public void segmentFailsToScan_ExecutorServiceIsShutdown() throws InterruptedException { + stubSuccessfulScan(0); + stubSuccessfulScan(1); + when(dynamoDB.scan(isSegmentNumber(2))) + .thenThrow(ProvisionedThroughputExceededException.builder().message("Slow Down!").build()); + stubSuccessfulScan(3); + stubSuccessfulScan(4); + + try { + parallelScanTask.getNextBatchOfScanResults(); + fail("Expected ProvisionedThroughputExceededException"); + } catch (ProvisionedThroughputExceededException expected) { + } + + executorService.awaitTermination(5, TimeUnit.SECONDS); + assertTrue(executorService.isShutdown()); + } + + /** + * A segment that returns a last-evaluated key must be scanned again with that key as the exclusive + * start key of its next page, otherwise the segment re-scans from the beginning. Drives one segment + * across two pages and asserts its second request resumes from the first page's last-evaluated key. + */ + @Test + public void segmentWithLastEvaluatedKey_scansNextPageFromThatKey() throws InterruptedException { + Map lastKey = + Collections.singletonMap("id", AttributeValue.builder().s("page1-last").build()); + + // Segment 0 spans two pages; the rest complete in one. + when(dynamoDB.scan(isSegmentNumber(0))) + .thenReturn(ScanResponse.builder().items(generateItems()).lastEvaluatedKey(lastKey).build()) + .thenReturn(ScanResponse.builder().items(generateItems()).build()); + stubSuccessfulScan(1); + stubSuccessfulScan(2); + stubSuccessfulScan(3); + stubSuccessfulScan(4); + + while (!parallelScanTask.isAllSegmentScanFinished()) { + parallelScanTask.getNextBatchOfScanResults(); + } + + ArgumentCaptor captor = ArgumentCaptor.forClass(ScanRequest.class); + verify(dynamoDB, atLeastOnce()).scan(captor.capture()); + + List segmentZeroRequests = new ArrayList(); + for (ScanRequest request : captor.getAllValues()) { + if (request.segment() == 0) { + segmentZeroRequests.add(request); + } + } + + assertEquals("segment 0 should have been scanned twice", 2, segmentZeroRequests.size()); + assertTrue("page 1 must not carry an exclusive start key", + segmentZeroRequests.get(0).exclusiveStartKey().isEmpty()); + assertEquals("page 2 must resume from page 1's last-evaluated key", + lastKey, segmentZeroRequests.get(1).exclusiveStartKey()); + } + + /** + * Stub a successful scan of a segment with a precanned item to return. + * + * @param segmentNumber Segment to stub. + */ + private void stubSuccessfulScan(int segmentNumber) { + when(dynamoDB.scan(isSegmentNumber(segmentNumber))) + .thenReturn(ScanResponse.builder().items(generateItems()).build()); + } + + private Map generateItems() { + final int numItems = 10; + Map items = new HashMap(numItems); + for (int i = 0; i < numItems; i++) { + items.put(UUID.randomUUID().toString(), AttributeValue.builder().s("foo").build()); + } + return items; + } + + private List createScanRequests() { + final List scanRequests = new ArrayList(TOTAL_SEGMENTS); + for (int i = 0; i < TOTAL_SEGMENTS; i++) { + scanRequests.add(createScanRequest(i)); + } + return scanRequests; + } + + private ScanRequest createScanRequest(int segmentNumber) { + return ScanRequest.builder() + .tableName(TABLE_NAME) + .segment(segmentNumber) + .totalSegments(TOTAL_SEGMENTS) + .build(); + } + + /** + * Custom matcher to match argument based on it's segment number + * + * @param segmentNumber Segment number to match for this stub. + * @return Stubbed argument matcher + */ + private static ScanRequest isSegmentNumber(int segmentNumber) { + return argThat(new SegmentArgumentMatcher(segmentNumber)); + } + + + /** + * Custom argument matcher to match a {@link ScanRequest} on the segment number. + */ + private static class SegmentArgumentMatcher implements ArgumentMatcher { + + private final int matchingSegmentNumber; + + private SegmentArgumentMatcher(int matchingSegmentNumber) { + this.matchingSegmentNumber = matchingSegmentNumber; + } + + @Override + public boolean matches(ScanRequest argument) { + if (argument == null) { + return false; + } + return matchingSegmentNumber == argument.segment(); + } + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/RandomUUIDMarshaller.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/RandomUUIDMarshaller.java new file mode 100644 index 000000000000..dafd81a86e30 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/RandomUUIDMarshaller.java @@ -0,0 +1,44 @@ +/* + * 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.mapper.dynamodb; + +import java.util.UUID; + +public class RandomUUIDMarshaller implements DynamoDBMarshaller, DynamoDBTypeConverter { + + public static final String randomUUID = UUID.randomUUID().toString(); + + @Override + public String marshall(Object getterReturnResult) { + return randomUUID; + } + + @Override + public Object unmarshall(Class clazz, String obj) { + return null; + } + + @Override + public String convert(final Object object) { + return randomUUID; + } + + @Override + public Object unconvert(final String object) { + return null; + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/S3LinkIDTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/S3LinkIDTest.java new file mode 100644 index 000000000000..b1d6bfecdcaf --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/S3LinkIDTest.java @@ -0,0 +1,48 @@ +/* + * 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.mapper.dynamodb; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import com.amazonaws.services.s3.model.Region; +import com.amazonaws.util.json.Jackson; + +public class S3LinkIDTest { + + @Test + public void testToFromJson() { + String region = "ap-northeast-1"; + S3Link.ID id = new S3Link.ID(region, "bucket", "key"); + String json = id.toJson(); + S3Link.ID twin = Jackson.fromJsonString(json, S3Link.ID.class); + assertEquals("bucket", twin.getBucket()); + assertEquals("key", twin.getKey()); + assertEquals(region, twin.getRegionId()); + } + + @Test + public void testDefaultRegion() { + S3Link.ID id = new S3Link.ID("bucketname", "keyname"); + assertEquals(Region.US_Standard.getFirstRegionId(), id.getRegionId()); + String json = id.toJson(); + S3Link.ID twin = Jackson.fromJsonString(json, S3Link.ID.class); + assertEquals("bucketname", twin.getBucket()); + assertEquals("keyname", twin.getKey()); + assertEquals(Region.US_Standard.getFirstRegionId(), twin.getRegionId()); + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/S3LinkTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/S3LinkTest.java new file mode 100644 index 000000000000..eef36dde4e24 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/S3LinkTest.java @@ -0,0 +1,151 @@ +/* + * 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.mapper.dynamodb; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; + +import org.junit.Before; +import org.junit.Test; + +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.internal.StaticCredentialsProvider; +import com.amazonaws.services.s3.model.Region; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; + +public class S3LinkTest +{ + private DynamoDBMapper mapper; + + @Before + public void setUp() { + AWSCredentials credentials = new BasicAWSCredentials("mock", "mock"); + DynamoDbClient db = mock(DynamoDbClient.class); + mapper = new DynamoDBMapper(db, new StaticCredentialsProvider(credentials)); + } + + @Test(expected=IllegalArgumentException.class) + public void nullKey() { + mapper.createS3Link("bucket", null); + } + + @Test(expected=IllegalArgumentException.class) + public void nullBucketName() { + mapper.createS3Link(null, "key"); + } + + @Test + public void testToJson() { + S3Link testLink = mapper.createS3Link("bucket","key"); + String json = testLink.toJson(); + + assertEquals(json, + "{\"s3\":{\"bucket\":\"bucket\",\"key\":\"key\",\"region\":null}}", + json); + testLink = mapper.createS3Link("bucket","testKey"); + json = testLink.toJson(); + assertEquals(json, + "{\"s3\":{\"bucket\":\"bucket\",\"key\":\"testKey\",\"region\":null}}", + json); + + testLink = mapper.createS3Link(Region.AP_Sydney, "bucket","testKey"); + json = testLink.toJson(); + assertEquals(json, + "{\"s3\":{\"bucket\":\"bucket\",\"key\":\"testKey\",\"region\":\"ap-southeast-2\"}}", + json); + + testLink = mapper.createS3Link(Region.AP_Sydney, "test-bucket","testKey"); + json = testLink.toJson(); + assertEquals(json, + "{\"s3\":{\"bucket\":\"test-bucket\",\"key\":\"testKey\",\"region\":\"ap-southeast-2\"}}", + json); + + testLink = mapper.createS3Link(Region.AP_Sydney, "test-bucket","test/key/with/slashes"); + json = testLink.toJson(); + assertEquals(json, + "{\"s3\":{\"bucket\":\"test-bucket\",\"key\":\"test/key/with/slashes\",\"region\":\"ap-southeast-2\"}}", + json); + + testLink = mapper.createS3Link("test-bucket","test/key/with/slashes"); + json = testLink.toJson(); + assertEquals(json, + "{\"s3\":{\"bucket\":\"test-bucket\",\"key\":\"test/key/with/slashes\",\"region\":null}}", + json); + testLink = mapper.createS3Link(Region.AP_Sydney, "test-bucket","test/key/with/slashes"); + json = testLink.toJson(); + assertEquals(json, + "{\"s3\":{\"bucket\":\"test-bucket\",\"key\":\"test/key/with/slashes\",\"region\":\"ap-southeast-2\"}}", + json); + } + + @Test + public void testFromJson() { + String json = "{\"s3\":{\"region\":\"ap-southeast-2\",\"bucket\":\"test-bucket\",\"key\":\"testKey\"}}"; + S3Link s3link = S3Link.fromJson(mapper.getS3ClientCache(), json); + assertEquals("test-bucket", s3link.getBucketName()); + assertEquals("ap-southeast-2", s3link.getRegion()); + assertEquals("testKey", s3link.getKey()); + } + + @Test + public void testDefaultRegion() { + S3Link testLink1 = mapper.createS3Link("bucket", "key"); + String json = testLink1.toJson(); + // Default to US_STANDARD if not specified + assertEquals(json, + "{\"s3\":{\"bucket\":\"bucket\",\"key\":\"key\",\"region\":null}}", + json); + // Default region changed to GovCloud + testLink1 = mapper.createS3Link(Region.US_GovCloud, "bucket", "key"); + json = testLink1.toJson(); + assertEquals(json, + "{\"s3\":{\"bucket\":\"bucket\",\"key\":\"key\",\"region\":\"us-gov-west-1\"}}", + json); + } + + @Test + public void testGetRegion_ReturnsUsEast1_WhenS3LinkCreated_WithNullRegion() { + S3Link s3Link = mapper.createS3Link("bucket", "key"); + + assertEquals("us-east-1", s3Link.getS3Region().toAWSRegion().getName()); + assertEquals("us-east-1", s3Link.getRegion()); + } + + @Test + public void testGetRegion_ReturnsUsEast1_WhenS3LinkCreated_WithUsStandardRegion() { + S3Link s3Link = mapper.createS3Link(Region.US_Standard, "bucket", "key"); + + assertEquals("us-east-1", s3Link.getS3Region().toAWSRegion().getName()); + assertEquals("us-east-1", s3Link.getRegion()); + } + + @Test + public void testGetRegion_ReturnsUsEast1_WhenS3LinkCreated_WithUsEast1Region() { + S3Link s3Link = mapper.createS3Link("us-east-1", "bucket", "key"); + + assertEquals("us-east-1", s3Link.getS3Region().toAWSRegion().getName()); + assertEquals("us-east-1", s3Link.getRegion()); + } + + @Test + public void testGetRegion_WithNonUsStandardRegion() { + S3Link s3Link = mapper.createS3Link(Region.EU_Frankfurt, "bucket", "key"); + + assertEquals(Region.EU_Frankfurt, s3Link.getS3Region()); + assertEquals(Region.EU_Frankfurt.toAWSRegion().getName(), s3Link.getRegion()); + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/StandardModelFactoriesEdgeCasesTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/StandardModelFactoriesEdgeCasesTest.java new file mode 100644 index 000000000000..77358daaba3c --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/StandardModelFactoriesEdgeCasesTest.java @@ -0,0 +1,240 @@ +/* + * 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.mapper.dynamodb; +import software.amazon.awssdk.mapper.dynamodb.internal.DynamoDBMapperModelFactory; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.nio.ByteBuffer; + +import org.junit.Assert; +import org.junit.Test; + +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.mapper.dynamodb.pojos.AutoKeyAndVal; +import software.amazon.awssdk.mapper.dynamodb.pojos.TestClass; + +/** + * Regression coverage for v1/v2 converter parity on edge inputs, beyond the single + * representative value each type gets in the other {@code StandardModelFactories*} tests. + */ +public class StandardModelFactoriesEdgeCasesTest { + + private static final DynamoDBMapperConfig V1_CONFIG = new DynamoDBMapperConfig.Builder() + .withTypeConverterFactory(DynamoDBMapperConfig.DEFAULT.getTypeConverterFactory()) + .withConversionSchema(ConversionSchemas.V1) + .build(); + + private static final DynamoDBMapperConfig V2_CONFIG = new DynamoDBMapperConfig.Builder() + .withTypeConverterFactory(DynamoDBMapperConfig.DEFAULT.getTypeConverterFactory()) + .withConversionSchema(ConversionSchemas.V2) + .build(); + + private static final DynamoDBMapperModelFactory factory = StandardModelFactories.of(S3Link.Factory.of(null)); + private static final DynamoDBMapperModelFactory.TableFactory v1Models = factory.getTableFactory(V1_CONFIG); + private static final DynamoDBMapperModelFactory.TableFactory v2Models = factory.getTableFactory(V2_CONFIG); + + private AttributeValue convert(String getter, Object value) { + try { + Method gm = TestClass.class.getMethod(getter); + StandardAnnotationMaps.FieldMap map = StandardAnnotationMaps.of(gm, null); + return v1Models.getTable(TestClass.class).field(map.attributeName()).convert(value); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private Object unconvert(String getter, String setter, AttributeValue value) { + try { + Method gm = TestClass.class.getMethod(getter); + Method sm = TestClass.class.getMethod(setter, gm.getReturnType()); + StandardAnnotationMaps.FieldMap map = StandardAnnotationMaps.of(gm, null); + return v2Models.getTable(TestClass.class).field(map.attributeName()).unconvert(value); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @SuppressWarnings("unchecked") + private static DynamoDBMapperTableModel table(T object) { + return v2Models.getTable((Class) object.getClass()); + } + + @Test + public void convert_withUnicodeString_preservesCharacters() { + assertEquals("こんにちは", convert("getString", "こんにちは").s()); + assertEquals("emoji-😀", convert("getString", "emoji-😀").s()); + } + + @Test + public void unconvert_withUnicodeString_preservesCharacters() { + assertEquals("こんにちは", + unconvert("getString", "setString", AttributeValue.builder().s("こんにちは").build())); + } + + @Test + public void convert_withEmptyString_returnsNull() { + Assert.assertNull(convert("getString", "")); + } + + @Test + public void convert_withNumericBoundaries_producesExactStrings() { + assertEquals(String.valueOf(Integer.MIN_VALUE), convert("getInt", Integer.MIN_VALUE).n()); + assertEquals(String.valueOf(Integer.MAX_VALUE), convert("getInt", Integer.MAX_VALUE).n()); + assertEquals(String.valueOf(Long.MIN_VALUE), convert("getLong", Long.MIN_VALUE).n()); + assertEquals(String.valueOf(Long.MAX_VALUE), convert("getLong", Long.MAX_VALUE).n()); + } + + @Test + public void convert_withBigNumbers_preservesPrecision() { + BigInteger big = new BigInteger("99999999999999999999"); + assertEquals("99999999999999999999", convert("getBigInt", big).n()); + + BigDecimal dec = new BigDecimal("12345.6789"); + assertEquals("12345.6789", convert("getBigDecimal", dec).n()); + } + + @Test + public void convert_withBigDecimal_preservesScale() { + assertEquals("43.0", convert("getBigDecimal", new BigDecimal("43.0")).n()); + } + + @Test + public void unconvert_withDecimalStringOnIntegralType_throwsNumberFormatException() { + try { + unconvert("getInt", "setInt", AttributeValue.builder().n("1.0").build()); + fail("Expected NumberFormatException for decimal string on integral type"); + } catch (NumberFormatException e) { + // expected + } + } + + @Test + public void convert_withNonFiniteDouble_producesExactStrings() { + assertEquals("NaN", convert("getDouble", Double.NaN).n()); + assertEquals("Infinity", convert("getDouble", Double.POSITIVE_INFINITY).n()); + assertEquals("-Infinity", convert("getDouble", Double.NEGATIVE_INFINITY).n()); + } + + public enum Color { RED, GREEN, BLUE } + + public static class EnumPojo extends AutoKeyAndVal { + @DynamoDBTypeConvertedEnum + public Color getVal() { return super.getVal(); } + public void setVal(Color val) { super.setVal(val); } + } + + @Test + public void convert_withEnum_producesString() { + DynamoDBMapperTableModel model = table(new EnumPojo()); + assertEquals("GREEN", model.field("val").convert(Color.GREEN).s()); + } + + @Test + public void unconvert_withEnumString_producesEnum() { + DynamoDBMapperTableModel model = table(new EnumPojo()); + assertEquals(Color.BLUE, model.field("val").unconvert(AttributeValue.builder().s("BLUE").build())); + } + + @Test + public void unconvert_withUnknownEnumValue_throwsException() { + DynamoDBMapperTableModel model = table(new EnumPojo()); + try { + model.field("val").unconvert(AttributeValue.builder().s("PURPLE").build()); + fail("Expected exception for unknown enum constant"); + } catch (RuntimeException e) { + // expected + } + } + + public static class JsonPayload { + private String name; + private int count; + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public int getCount() { return count; } + public void setCount(int count) { this.count = count; } + @Override + public boolean equals(Object o) { + if (!(o instanceof JsonPayload)) { + return false; + } + JsonPayload that = (JsonPayload) o; + return count == that.count + && (name == null ? that.name == null : name.equals(that.name)); + } + @Override + public int hashCode() { + return (name == null ? 0 : name.hashCode()) * 31 + count; + } + } + + public static class JsonPojo extends AutoKeyAndVal { + @DynamoDBTypeConvertedJson + public JsonPayload getVal() { return super.getVal(); } + public void setVal(JsonPayload val) { super.setVal(val); } + } + + @Test + public void convert_withJsonPayload_producesString() { + JsonPayload payload = new JsonPayload(); + payload.setName("widget"); + payload.setCount(7); + + DynamoDBMapperTableModel model = table(new JsonPojo()); + AttributeValue av = model.field("val").convert(payload); + + Assert.assertNotNull(av.s()); + Assert.assertTrue(av.s().contains("widget")); + Assert.assertTrue(av.s().contains("7")); + } + + @Test + public void convertAndUnconvert_withJsonPayload_roundTrips() { + JsonPayload payload = new JsonPayload(); + payload.setName("widget"); + payload.setCount(7); + + DynamoDBMapperTableModel model = table(new JsonPojo()); + AttributeValue av = model.field("val").convert(payload); + Object back = model.field("val").unconvert(av); + + assertEquals(payload, back); + } + + @Test + public void unconvert_withByteBuffer_returnsWritableBuffer() { + // v1 parity: v1 getB() returned a writable buffer. v2 SdkBytes.asByteBuffer() is + // read-only, so the unmarshaller returns a writable copy for callers that mutate it. + DynamoDBMapperTableModel model = v2Models.getTable(TestClass.class); + AttributeValue stored = AttributeValue.builder().b(SdkBytes.fromUtf8String("data")).build(); + + ByteBuffer result = (ByteBuffer) model.field("byteBuffer").unconvert(stored); + + Assert.assertFalse("returned buffer is writable (v1 parity)", result.isReadOnly()); + result.put(0, (byte) 'X'); + assertEquals((byte) 'X', result.get(0)); + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/StandardModelFactoriesOverrideTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/StandardModelFactoriesOverrideTest.java new file mode 100644 index 000000000000..d98d1a296c48 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/StandardModelFactoriesOverrideTest.java @@ -0,0 +1,39 @@ +/* + * 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.mapper.dynamodb; +import software.amazon.awssdk.mapper.dynamodb.internal.DynamoDBMapperModelFactory; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +import java.lang.reflect.Method; + +public class StandardModelFactoriesOverrideTest extends StandardModelFactoriesV2Test { + + private final DynamoDBMapperConfig config = new DynamoDBMapperConfig.Builder() + .withTypeConverterFactory(DynamoDBMapperConfig.DEFAULT.getTypeConverterFactory()) + .withConversionSchema(ConversionSchemas.v2Builder("V2Override").build()) + .build(); + + private final DynamoDBMapperModelFactory factory = StandardModelFactories.of(S3Link.Factory.of(null)); + private final DynamoDBMapperModelFactory.TableFactory models = factory.getTableFactory(config); + + @Override + protected AttributeValue convert(Class clazz, Method getter, Object value) { + final StandardAnnotationMaps.FieldMap map = StandardAnnotationMaps.of(getter, null); + return models.getTable(clazz).field(map.attributeName()).convert(value); + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/StandardModelFactoriesTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/StandardModelFactoriesTest.java new file mode 100644 index 000000000000..ee51fc094058 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/StandardModelFactoriesTest.java @@ -0,0 +1,1455 @@ +/* + * 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.mapper.dynamodb; +import software.amazon.awssdk.mapper.dynamodb.internal.DynamoDBMapperModelFactory; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperFieldModel.DynamoDBAttributeType; +import software.amazon.awssdk.mapper.dynamodb.pojos.AutoKeyAndVal; +import software.amazon.awssdk.mapper.dynamodb.pojos.Currency; +import software.amazon.awssdk.mapper.dynamodb.pojos.DateRange; +import software.amazon.awssdk.mapper.dynamodb.pojos.KeyAndVal; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.KeyType; +import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +/** + * Unit tests for {@link DynamoDBMapperModelFactory.TableFactory}. + */ +public class StandardModelFactoriesTest { + + private static final DynamoDBMapperModelFactory factory = StandardModelFactories.of(S3Link.Factory.of(null)); + private static final DynamoDBMapperModelFactory.TableFactory models = factory.getTableFactory(DynamoDBMapperConfig.DEFAULT); + + @SuppressWarnings("unchecked") + private static DynamoDBMapperTableModel getTable(T object) { + return models.getTable((Class)object.getClass()); + } + + /** + * Test mappings. + */ + @Test + public void testHashAndRangeKey() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBHashKey(attributeName="hk") + public String getKey() { return super.getKey(); } + @DynamoDBRangeKey(attributeName="rk") + public String getVal() { return super.getVal(); } + public void setVal(final String val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + assertFieldKeyType(KeyType.HASH, model.field("hk"), model); + assertFieldKeyType(KeyType.RANGE, model.field("rk"), model); + } + + /** + * Test mappings. + */ + @Test(expected=DynamoDBMappingException.class) + public void testHashAndRangeKeyConflict() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBHashKey @DynamoDBRangeKey + public String getKey() { return super.getKey(); } + }; + getTable(obj); + } + + /** + * Test mappings. + */ + @Test + public void testNamed() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBNamed("value") + public String getVal() { return super.getVal(); } + public void setVal(final String val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + assertEquals(2, model.fields().size()); + assertNotNull(model.field("key")); + assertNotNull(model.field("value")); + } + + /** + * Test mappings. + */ + @Test + public void testAttributeTypeAsNumber() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBTyped(DynamoDBAttributeType.N) + public String getVal() { return super.getVal(); } + public void setVal(final String val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel val = model.field("val"); + assertEquals(DynamoDBAttributeType.N, val.attributeType()); + } + + @Test + public void testAttributeTypeAsAttributeValueNumber() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBTyped(DynamoDBAttributeType.N) + public AttributeValue getVal() { return super.getVal(); } + public void setVal(final AttributeValue val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel val = model.field("val"); + assertEquals(DynamoDBAttributeType.N, val.attributeType()); + assertEquals("123", val.convert(AttributeValue.builder().n("123").build()).n()); + assertEquals("123", val.unconvert(AttributeValue.builder().n("123").build()).n()); + } + + @Test + public void testAttributeTypeAsAttributeValueMap() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBTyped(DynamoDBAttributeType.M) + public AttributeValue getVal() { return super.getVal(); } + public void setVal(final AttributeValue val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel val = model.field("val"); + assertEquals(DynamoDBAttributeType.M, val.attributeType()); + + Map map = new HashMap(); + map.put("A", AttributeValue.builder().n("123").build()); + map = Collections.unmodifiableMap(map); + + assertEquals("123", val.convert(AttributeValue.builder().m(map).build()).m().get("A").n()); + assertEquals("123", val.unconvert(AttributeValue.builder().m(map).build()).m().get("A").n()); + } + + /** + * Test mappings. + */ + @Test + public void testScalarAttributeStringTimeZone() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBHashKey + public String getKey() { return super.getKey(); } + @DynamoDBScalarAttribute(type=ScalarAttributeType.S) + public TimeZone getVal() { return super.getVal(); } + public void setVal(final TimeZone val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel val = model.field("val"); + assertEquals(DynamoDBAttributeType.S, val.attributeType()); + assertEquals("America/New_York", val.convert(TimeZone.getTimeZone("America/New_York")).s()); + assertEquals("America/New_York", val.unconvert(AttributeValue.builder().s("America/New_York").build()).getID()); + } + + /** + * Test mappings. + */ + @Test + public void testScalarAttributeStringLocale() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBHashKey + public String getKey() { return super.getKey(); } + @DynamoDBScalarAttribute(type=ScalarAttributeType.S) + public Locale getVal() { return super.getVal(); } + public void setVal(final Locale val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel val = model.field("val"); + assertEquals(DynamoDBAttributeType.S, val.attributeType()); + assertEquals("en-CA", val.convert(new Locale("en","CA")).s()); + assertEquals("en-CA", val.unconvert(AttributeValue.builder().s("en-CA").build()).toString().replaceAll("_","-")); + } + + /** + * Test mappings. + */ + @Test + public void testScalarAttributeBinaryUuid() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBHashKey + public String getKey() { return super.getKey(); } + @DynamoDBScalarAttribute(type=ScalarAttributeType.B) + public UUID getVal() { return super.getVal(); } + public void setVal(final UUID val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + assertEquals(DynamoDBAttributeType.B, model.field("val").attributeType()); + final UUID val = UUID.randomUUID(); + final AttributeValue converted = model.field("val").convert(val); + assertNotNull(converted.b()); + assertEquals(val, model.field("val").unconvert(converted)); + } + + @Test + public void testScalarAttributeAttributeName() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBHashKey + public String getKey() { return super.getKey(); } + @DynamoDBScalarAttribute(attributeName="value", type=ScalarAttributeType.S) + public String getVal() { return super.getVal(); } + public void setVal(final String val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = models.getTable((Class)obj.getClass()); + final DynamoDBMapperFieldModel val = model.field("value"); + assertEquals(DynamoDBAttributeType.S, val.attributeType()); + } + + /** + * Test mappings. + */ + @Test + public void testIgnore() { + final Object obj = new AutoKeyAndVal() { + private String ignore; + @DynamoDBAttribute(attributeName="value") + public String getVal() { return super.getVal(); } + public void setVal(final String val) { super.setVal(val); } + @DynamoDBIgnore @DynamoDBAttribute(attributeName="ignore") + public String getIgnore() { return this.ignore; } + public void setIgnore(final String ignore) { this.ignore = ignore; } + }; + final DynamoDBMapperTableModel model = getTable(obj); + assertEquals(2, model.fields().size()); + assertNotNull(model.field("key")); + assertNotNull(model.field("value")); + } + + /** + * Test mappings. + */ + @Test + public void testConvertedBool() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBConvertedBool(DynamoDBConvertedBool.Format.Y_N) + public Boolean getVal() { return super.getVal(); } + public void setVal(final Boolean val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel val = model.field("val"); + assertEquals(DynamoDBAttributeType.S, val.attributeType()); + assertEquals("Y", val.convert(Boolean.TRUE).s()); + assertEquals(Boolean.TRUE, val.unconvert(AttributeValue.builder().s("Y").build())); + assertEquals("N", val.convert(Boolean.FALSE).s()); + assertEquals(Boolean.FALSE, val.unconvert(AttributeValue.builder().s("N").build())); + assertEquals(null, val.convert(null)); + } + + /** + * Test mappings. + */ + @Test + public void testAutoGeneratedHashKeyString() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBAttribute + public String getVal() { return super.getVal(); } + public void setVal(final String val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel key = model.field("key"); + assertFieldKeyType(KeyType.HASH, key, model); + assertEquals(DynamoDBAutoGenerateStrategy.CREATE, key.getGenerateStrategy()); + assertNotNull(key.generate(null)); + assertNotNull(key.generate(UUID.randomUUID().toString())); + } + + /** + * Test mappings. + */ + @Test + public void testAutoGeneratedRangeKeyUuid() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBRangeKey @DynamoDBAutoGeneratedKey + public UUID getVal() { return super.getVal(); } + public void setVal(final UUID val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel val = model.field("val"); + assertFieldKeyType(KeyType.RANGE, val, model); + assertEquals(DynamoDBAutoGenerateStrategy.CREATE, val.getGenerateStrategy()); + assertNotNull(val.generate(null)); + assertNotNull(val.generate(UUID.randomUUID())); + } + + /** + * Test mappings. + */ + @Test(expected=DynamoDBMappingException.class) + public void testAutoGeneratedConflict() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBHashKey @DynamoDBAutoGeneratedKey @DynamoDBVersionAttribute + public String getKey() { return super.getKey(); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + } + + /** + * Test mappings. + */ + @Test(expected=DynamoDBMappingException.class) + public void testAutoGeneratedVersionUuid() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBVersionAttribute + public UUID getVal() { return super.getVal(); } + public void setVal(final UUID val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel val = model.field("val"); + val.generate(null); //<- should fail + } + + /** + * Test mappings. + */ + @Test + public void testAutoGeneratedVersionBigInteger() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBVersionAttribute + public BigInteger getVal() { return super.getVal(); } + public void setVal(final BigInteger val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel val = model.field("val"); + assertEquals(true, val.versioned()); + assertEquals(DynamoDBAutoGenerateStrategy.ALWAYS, val.getGenerateStrategy()); + assertEquals(BigInteger.ONE, val.generate(null)); + assertEquals(BigInteger.valueOf((int)2), val.generate(BigInteger.ONE)); + } + + /** + * Test mappings. + */ + @Test + public void testAutoGeneratedVersionByte() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBVersionAttribute + public Byte getVal() { return super.getVal(); } + public void setVal(final Byte val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel val = model.field("val"); + assertEquals(true, val.versioned()); + assertEquals(DynamoDBAutoGenerateStrategy.ALWAYS, val.getGenerateStrategy()); + assertEquals(Byte.valueOf((byte)1), val.generate(null)); + assertEquals(Byte.valueOf((byte)2), val.generate(Byte.valueOf((byte)1))); + } + + /** + * Test mappings. + */ + @Test + public void testAutoGeneratedVersionBytePrimitive() { + final Object obj = new AutoKeyAndVal() { + private byte rvn; + @DynamoDBAttribute + public String getVal() { return super.getVal(); } + public void setVal(final String val) { super.setVal(val); } + @DynamoDBVersionAttribute + public byte getRvn() { return this.rvn; } + public void setRvn(final byte rvn) { this.rvn = rvn; } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel rvn = model.field("rvn"); + assertEquals(true, rvn.versioned()); + assertEquals(DynamoDBAutoGenerateStrategy.ALWAYS, rvn.getGenerateStrategy()); + assertEquals(Byte.valueOf((byte)1), rvn.generate(null)); + assertEquals(Byte.valueOf((byte)2), rvn.generate(Byte.valueOf((byte)1))); + } + + /** + * Test mappings. + */ + @Test + public void testAutoGeneratedVersionInteger() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBVersionAttribute + public Integer getVal() { return super.getVal(); } + public void setVal(final Integer val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel val = model.field("val"); + assertEquals(true, val.versioned()); + assertEquals(DynamoDBAutoGenerateStrategy.ALWAYS, val.getGenerateStrategy()); + assertEquals(Integer.valueOf((int)1), val.generate(null)); + assertEquals(Integer.valueOf((int)2), val.generate(Integer.valueOf((int)1))); + } + + /** + * Test mappings. + */ + @Test + public void testAutoGeneratedVersionIntegerPrimitive() { + final Object obj = new AutoKeyAndVal() { + private int rvn; + @DynamoDBAttribute + public String getVal() { return super.getVal(); } + public void setVal(final String val) { super.setVal(val); } + @DynamoDBVersionAttribute + public int getRvn() { return this.rvn; } + public void setRvn(final int rvn) { this.rvn = rvn; } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel rvn = model.field("rvn"); + assertEquals(true, rvn.versioned()); + assertEquals(DynamoDBAutoGenerateStrategy.ALWAYS, rvn.getGenerateStrategy()); + assertEquals(Integer.valueOf((int)1), rvn.generate(null)); + assertEquals(Integer.valueOf((int)2), rvn.generate(Integer.valueOf((int)1))); + } + + /** + * Test mappings. + */ + @Test + public void testAutoGeneratedVersionLong() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBVersionAttribute + public Long getVal() { return super.getVal(); } + public void setVal(final Long val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel val = model.field("val"); + assertEquals(true, val.versioned()); + assertEquals(DynamoDBAutoGenerateStrategy.ALWAYS, val.getGenerateStrategy()); + assertEquals(Long.valueOf((long)1), val.generate(null)); + assertEquals(Long.valueOf((long)2), val.generate(Long.valueOf((long)1))); + } + + /** + * Test mappings. + */ + @Test + public void testAutoGeneratedVersionLongPrimitive() { + final Object obj = new AutoKeyAndVal() { + private long rvn; + @DynamoDBAttribute + public String getVal() { return super.getVal(); } + public void setVal(final String val) { super.setVal(val); } + @DynamoDBVersionAttribute + public long getRvn() { return this.rvn; } + public void setRvn(final long rvn) { this.rvn = rvn; } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel rvn = model.field("rvn"); + assertEquals(true, rvn.versioned()); + assertEquals(DynamoDBAutoGenerateStrategy.ALWAYS, rvn.getGenerateStrategy()); + assertEquals(Long.valueOf((long)1), rvn.generate(null)); + assertEquals(Long.valueOf((long)2), rvn.generate(Long.valueOf((long)1))); + } + + /** + * Test mappings. + */ + @Test + public void testAutoGeneratedVersionShort() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBVersionAttribute + public Short getVal() { return super.getVal(); } + public void setVal(final Short val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel val = model.field("val"); + assertEquals(true, val.versioned()); + assertEquals(DynamoDBAutoGenerateStrategy.ALWAYS, val.getGenerateStrategy()); + assertEquals(Short.valueOf((short)1), val.generate(null)); + assertEquals(Short.valueOf((short)2), val.generate(Short.valueOf((short)1))); + } + + /** + * Test mappings. + */ + @Test + public void testAutoGeneratedVersionShortPrimitive() { + final Object obj = new AutoKeyAndVal() { + private short rvn; + @DynamoDBAttribute + public String getVal() { return super.getVal(); } + public void setVal(final String val) { super.setVal(val); } + @DynamoDBVersionAttribute + public short getRvn() { return this.rvn; } + public void setRvn(final short rvn) { this.rvn = rvn; } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel rvn = model.field("rvn"); + assertEquals(true, rvn.versioned()); + assertEquals(DynamoDBAutoGenerateStrategy.ALWAYS, rvn.getGenerateStrategy()); + assertEquals(Short.valueOf((short)1), rvn.generate(null)); + assertEquals(Short.valueOf((short)2), rvn.generate(Short.valueOf((short)1))); + } + + /** + * Test mappings. + */ + @Test(expected=DynamoDBMappingException.class) + public void testAutoGeneratedTimestampUuid() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBAutoGeneratedTimestamp + public UUID getVal() { return super.getVal(); } + public void setVal(final UUID val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + } + + /** + * Test mappings. + */ + @Test + public void testAutoGeneratedTimestampCalendar() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBAutoGeneratedTimestamp + public Calendar getVal() { return super.getVal(); } + public void setVal(final Calendar val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel val = model.field("val"); + assertEquals(DynamoDBAutoGenerateStrategy.ALWAYS, val.getGenerateStrategy()); + assertNotNull(val.generate(null)); + assertNotNull(val.generate(Calendar.getInstance())); + } + + /** + * Test mappings. + */ + @Test + public void testAutoGeneratedTimestampDateKey() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBRangeKey @DynamoDBAutoGeneratedTimestamp(strategy=DynamoDBAutoGenerateStrategy.CREATE) + public Date getVal() { return super.getVal(); } + public void setVal(final Date val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel val = model.field("val"); + assertFieldKeyType(KeyType.RANGE, val, model); + assertEquals(DynamoDBAutoGenerateStrategy.CREATE, val.getGenerateStrategy()); + assertNotNull(val.generate(null)); + assertNotNull(val.generate(new Date())); + } + + /** + * Test mappings. + */ + @Test + public void testAutoGeneratedTimestampDateVal() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBAutoGeneratedTimestamp + public Date getVal() { return super.getVal(); } + public void setVal(final Date val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel val = model.field("val"); + assertEquals(DynamoDBAutoGenerateStrategy.ALWAYS, val.getGenerateStrategy()); + assertNotNull(val.generate(null)); + assertNotNull(val.generate(new Date())); + } + + /** + * Test mappings. + */ + @Test + public void testAutoGeneratedTimestampLong() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBAutoGeneratedTimestamp + public Long getVal() { return super.getVal(); } + public void setVal(final Long val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel val = model.field("val"); + assertEquals(DynamoDBAutoGenerateStrategy.ALWAYS, val.getGenerateStrategy()); + assertNotNull(val.generate(null)); + assertNotNull(val.generate(System.currentTimeMillis())); + } + + /** + * Test mappings. + */ + @Test + public void testAutoGeneratedDefaultByteBuffer() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBAutoGeneratedDefault("default-val") + public ByteBuffer getVal() { return super.getVal(); } + public void setVal(final ByteBuffer val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel val = model.field("val"); + assertEquals(DynamoDBAutoGenerateStrategy.CREATE, val.getGenerateStrategy()); + assertNotNull(val.generate(null)); + } + + /** + * Test mappings. + */ + @Test + public void testAutoGeneratedDefaultBigDecimal() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBAutoGeneratedDefault("1234.5") + public BigDecimal getVal() { return super.getVal(); } + public void setVal(final BigDecimal val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel val = model.field("val"); + assertEquals(DynamoDBAutoGenerateStrategy.CREATE, val.getGenerateStrategy()); + assertEquals(BigDecimal.valueOf(1234.5D), val.generate(null)); + } + + /** + * Test mappings. + */ + @Test + public void testAutoGeneratedDefaultBigInteger() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBAutoGeneratedDefault("1234") + public BigInteger getVal() { return super.getVal(); } + public void setVal(final BigInteger val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel val = model.field("val"); + assertEquals(DynamoDBAutoGenerateStrategy.CREATE, val.getGenerateStrategy()); + assertEquals(BigInteger.valueOf(1234), val.generate(null)); + } + + /** + * Test mappings. + */ + @Test + public void testAutoGeneratedDefaultBoolean_true() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBAutoGeneratedDefault("true") + public Boolean getVal() { return super.getVal(); } + public void setVal(final Boolean val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel val = model.field("val"); + assertEquals(DynamoDBAutoGenerateStrategy.CREATE, val.getGenerateStrategy()); + assertEquals(Boolean.TRUE, val.generate(null)); + } + + /** + * Test mappings. + */ + @Test + public void testAutoGeneratedDefaultBoolean_0() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBAutoGeneratedDefault("0") + public Boolean getVal() { return super.getVal(); } + public void setVal(final Boolean val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel val = model.field("val"); + assertEquals(DynamoDBAutoGenerateStrategy.CREATE, val.getGenerateStrategy()); + assertEquals(Boolean.FALSE, val.generate(null)); + } + + /** + * Test mappings. + */ + @Test + public void testAutoGeneratedDefaultBoolean_1() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBAutoGeneratedDefault("1") + public Boolean getVal() { return super.getVal(); } + public void setVal(final Boolean val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel val = model.field("val"); + assertEquals(DynamoDBAutoGenerateStrategy.CREATE, val.getGenerateStrategy()); + assertEquals(Boolean.TRUE, val.generate(null)); + } + + /** + * Test mappings. + */ + @Test + public void testAutoGeneratedDefaultBoolean_y() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBAutoGeneratedDefault("y") + public Boolean getVal() { return super.getVal(); } + public void setVal(final Boolean val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel val = model.field("val"); + assertEquals(DynamoDBAutoGenerateStrategy.CREATE, val.getGenerateStrategy()); + assertEquals(Boolean.TRUE, val.generate(null)); + } + + /** + * Test mappings. + */ + @Test + public void testAutoGeneratedDefaultBoolean_Y() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBAutoGeneratedDefault("Y") + public Boolean getVal() { return super.getVal(); } + public void setVal(final Boolean val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel val = model.field("val"); + assertEquals(DynamoDBAutoGenerateStrategy.CREATE, val.getGenerateStrategy()); + assertEquals(Boolean.TRUE, val.generate(null)); + } + + /** + * Test mappings. + */ + @Test + public void testAutoGeneratedDefaultByte() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBAutoGeneratedDefault("1") + public Byte getVal() { return super.getVal(); } + public void setVal(final Byte val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel val = model.field("val"); + assertEquals(DynamoDBAutoGenerateStrategy.CREATE, val.getGenerateStrategy()); + assertEquals(Byte.valueOf((byte)1), val.generate(null)); + } + + /** + * Test mappings. + */ + @Test + public void testAutoGeneratedDefaultCharacter() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBAutoGeneratedDefault("A") + public Character getVal() { return super.getVal(); } + public void setVal(final Character val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel val = model.field("val"); + assertEquals(DynamoDBAutoGenerateStrategy.CREATE, val.getGenerateStrategy()); + assertEquals(Character.valueOf('A'), val.generate(null)); + } + + /** + * Test mappings. + */ + @Test + public void testAutoGeneratedDefaultCurrency() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBAutoGeneratedDefault("CAD") + public java.util.Currency getVal() { return super.getVal(); } + public void setVal(final java.util.Currency val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel val = model.field("val"); + assertEquals(DynamoDBAutoGenerateStrategy.CREATE, val.getGenerateStrategy()); + assertEquals(java.util.Currency.getInstance("CAD"), val.generate(null)); + } + + /** + * Test mappings. + */ + @Test + public void testAutoGeneratedDefaultDouble() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBAutoGeneratedDefault("1234.5") + public Double getVal() { return super.getVal(); } + public void setVal(final Double val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel val = model.field("val"); + assertEquals(DynamoDBAutoGenerateStrategy.CREATE, val.getGenerateStrategy()); + assertEquals(Double.valueOf(1234.5D), val.generate(null)); + } + + /** + * Test mappings. + */ + @Test + public void testAutoGeneratedDefaultEnum() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBTypeConvertedEnum + @DynamoDBAutoGeneratedDefault("SECONDS") + public TimeUnit getVal() { return super.getVal(); } + public void setVal(final TimeUnit val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel val = model.field("val"); + assertEquals(DynamoDBAutoGenerateStrategy.CREATE, val.getGenerateStrategy()); + assertEquals(TimeUnit.SECONDS, val.generate(null)); + assertEquals(TimeUnit.SECONDS, val.generate(TimeUnit.MILLISECONDS)); + } + + /** + * Test mappings. + */ + @Test + public void testAutoGeneratedDefaultFloat() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBAutoGeneratedDefault("1234.5") + public Float getVal() { return super.getVal(); } + public void setVal(final Float val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel val = model.field("val"); + assertEquals(DynamoDBAutoGenerateStrategy.CREATE, val.getGenerateStrategy()); + assertEquals(Float.valueOf(1234.5F), val.generate(null)); + } + + /** + * Test mappings. + */ + @Test + public void testAutoGeneratedDefaultInteger() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBAutoGeneratedDefault("1234") + public Integer getVal() { return super.getVal(); } + public void setVal(final Integer val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel val = model.field("val"); + assertEquals(DynamoDBAutoGenerateStrategy.CREATE, val.getGenerateStrategy()); + assertEquals(Integer.valueOf((int)1234), val.generate(null)); + } + + /** + * Test mappings. + */ + @Test + public void testAutoGeneratedDefaultLong() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBAutoGeneratedDefault("1234") + public Long getVal() { return super.getVal(); } + public void setVal(final Long val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel val = model.field("val"); + assertEquals(DynamoDBAutoGenerateStrategy.CREATE, val.getGenerateStrategy()); + assertEquals(Long.valueOf((long)1234), val.generate(null)); + } + + /** + * Test mappings. + */ + @Test + public void testAutoGeneratedDefaultShort() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBAutoGeneratedDefault("1234") + public Short getVal() { return super.getVal(); } + public void setVal(final Short val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel val = model.field("val"); + assertEquals(DynamoDBAutoGenerateStrategy.CREATE, val.getGenerateStrategy()); + assertEquals(Short.valueOf((short)1234), val.generate(null)); + } + + /** + * Test mappings. + */ + @Test + public void testAutoGeneratedDefaultString() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBAutoGeneratedDefault("default-val") + public String getVal() { return super.getVal(); } + public void setVal(final String val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel val = model.field("val"); + assertEquals(DynamoDBAutoGenerateStrategy.CREATE, val.getGenerateStrategy()); + assertEquals("default-val", val.generate(null)); + assertEquals("default-val", val.generate("not-default")); + } + + /** + * Test mappings. + */ + @Test + public void testAutoGeneratedDefaultTimeZone() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBAutoGeneratedDefault("America/New_York") + public TimeZone getVal() { return super.getVal(); } + public void setVal(final TimeZone val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel val = model.field("val"); + assertEquals(DynamoDBAutoGenerateStrategy.CREATE, val.getGenerateStrategy()); + assertEquals(TimeZone.getTimeZone("America/New_York"), val.generate(null)); + assertEquals(TimeZone.getTimeZone("America/New_York"), val.generate(TimeZone.getTimeZone("America/Los_Angeles"))); + } + + /** + * Test mappings. + */ + @Test + public void testAutoGeneratedDefaultUuid() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBAutoGeneratedDefault("12345678-1234-1234-1234-123456789012") + public UUID getVal() { return super.getVal(); } + public void setVal(final UUID val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel val = model.field("val"); + assertEquals(DynamoDBAutoGenerateStrategy.CREATE, val.getGenerateStrategy()); + assertEquals(UUID.fromString("12345678-1234-1234-1234-123456789012"), val.generate(null)); + } + + /** + * Test mappings. + */ + @Test + public void testIndexHashKeyGlobalSecondaryIndexName() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBIndexHashKey(attributeName="gsi_hk", globalSecondaryIndexName="gsi") + public String getVal() { return super.getVal(); } + public void setVal(final String val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel gsi_hk = model.field("gsi_hk"); + assertFieldGsiNames(Arrays.asList("gsi"), KeyType.HASH, gsi_hk, model); + assertFieldGsiNames(null, KeyType.RANGE, gsi_hk, model); + assertFieldLsiNames(null, gsi_hk, model); + } + + /** + * Test mappings. + */ + @Test + public void testIndexHashKeyGlobalSecondaryIndexNames() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBIndexHashKey(attributeName="gsi_hk", globalSecondaryIndexNames="gsi") + public String getVal() { return super.getVal(); } + public void setVal(final String val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel gsi_hk = model.field("gsi_hk"); + assertFieldGsiNames(Arrays.asList("gsi"), KeyType.HASH, gsi_hk, model); + assertFieldGsiNames(null, KeyType.RANGE, gsi_hk, model); + assertFieldLsiNames(null, gsi_hk, model); + } + + /** + * Test mappings. + */ + @Test + public void testIndexRangeKeyGlobalSecondaryIndexName() { + final Object obj = new AutoKeyAndVal() { + private String gsi; + @DynamoDBIndexHashKey(attributeName="gsi_hk", globalSecondaryIndexName="gsi") + public String getVal() { return super.getVal(); } + public void setVal(final String val) { super.setVal(val); } + @DynamoDBIndexRangeKey(attributeName="gsi_rk", globalSecondaryIndexName="gsi") + public String getGsi() { return this.gsi; } + public void setGsi(final String gsi) { this.gsi = gsi; } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel gsi_hk = model.field("gsi_hk"); + assertFieldGsiNames(Arrays.asList("gsi"), KeyType.HASH, gsi_hk, model); + assertFieldGsiNames(null, KeyType.RANGE, gsi_hk, model); + assertFieldLsiNames(null, gsi_hk, model); + final DynamoDBMapperFieldModel gsi_rk = model.field("gsi_rk"); + assertFieldGsiNames(null, KeyType.HASH, gsi_rk, model); + assertFieldGsiNames(Arrays.asList("gsi"), KeyType.RANGE, gsi_rk, model); + assertFieldLsiNames(null, gsi_rk, model); + } + + /** + * Test mappings. + */ + @Test + public void testIndexRangeKeyGlobalSecondaryIndexNames() { + final Object obj = new AutoKeyAndVal() { + private String gsi; + @DynamoDBIndexHashKey(attributeName="gsi_hk", globalSecondaryIndexName="gsi") + public String getVal() { return super.getVal(); } + public void setVal(final String val) { super.setVal(val); } + @DynamoDBIndexRangeKey(attributeName="gsi_rk", globalSecondaryIndexNames="gsi") + public String getGsi() { return this.gsi; } + public void setGsi(final String gsi) { this.gsi = gsi; } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel gsi_hk = model.field("gsi_hk"); + assertFieldGsiNames(Arrays.asList("gsi"), KeyType.HASH, gsi_hk, model); + assertFieldGsiNames(null, KeyType.RANGE, gsi_hk, model); + assertFieldLsiNames(null, gsi_hk, model); + final DynamoDBMapperFieldModel gsi_rk = model.field("gsi_rk"); + assertFieldGsiNames(null, KeyType.HASH, gsi_rk, model); + assertFieldGsiNames(Arrays.asList("gsi"), KeyType.RANGE, gsi_rk, model); + assertFieldLsiNames(null, gsi_rk, model); + } + + /** + * Test mappings. + */ + @Test + public void testIndexRangeKeyiLocalSecondaryIndexName() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBIndexRangeKey(attributeName="lsi_rk", localSecondaryIndexName="lsi") + public String getVal() { return super.getVal(); } + public void setVal(final String val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel lsi_rk = model.field("lsi_rk"); + assertFieldLsiNames(Arrays.asList("lsi"), lsi_rk, model); + } + + /** + * Test mappings. + */ + @Test + public void testIndexRangeKeyLocalSecondaryIndexNames() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBIndexRangeKey(attributeName="lsi_rk", localSecondaryIndexNames="lsi") + public String getVal() { return super.getVal(); } + public void setVal(final String val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + final DynamoDBMapperFieldModel lsi_rk = model.field("lsi_rk"); + assertFieldLsiNames(Arrays.asList("lsi"), lsi_rk, model); + } + + /** + * Test mappings. + */ + @Test + public void testFlattened() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBFlattened(attributes={ + @DynamoDBAttribute(mappedBy="start", attributeName="DateRangeStart"), + @DynamoDBAttribute(mappedBy="end", attributeName="DateRangeEnd")}) + public DateRange getVal() { return super.getVal(); } + public void setVal(final DateRange val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + assertEquals(3, model.fields().size()); + assertEquals("DateRangeStart", model.field("DateRangeStart").name()); + assertEquals("DateRangeEnd", model.field("DateRangeEnd").name()); + } + + /** + * Test mappings. + */ + @Test + public void testFlattenedNotAllSpecified() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBFlattened(attributes={ + @DynamoDBAttribute(mappedBy="start", attributeName="DateRangeStart")}) + public DateRange getVal() { return super.getVal(); } + public void setVal(final DateRange val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + assertEquals(2, model.fields().size()); + assertEquals("DateRangeStart", model.field("DateRangeStart").name()); + } + + /** + * Test mappings. + */ + @Test(expected=DynamoDBMappingException.class) + public void testFlattenedInvalidMappedBy() { + final Object obj = new AutoKeyAndVal() { + @DynamoDBFlattened(attributes={ + @DynamoDBAttribute(mappedBy="xstart", attributeName="DateRangeStart"), + @DynamoDBAttribute(mappedBy="xend", attributeName="DateRangeEnd")}) + public DateRange getVal() { return super.getVal(); } + public void setVal(final DateRange val) { super.setVal(val); } + }; + getTable(obj); + } + + /** + * Test mappings. + */ + @Test + public void testFlattenedMultipleSameType() { + final Object obj = new AutoKeyAndVal() { + private Currency other; + @DynamoDBFlattened(attributes={ + @DynamoDBAttribute(mappedBy="amount", attributeName="firstAmount"), + @DynamoDBAttribute(mappedBy="unit", attributeName="firstUnit")}) + public Currency getVal() { return super.getVal(); } + public void setVal(final Currency val) { super.setVal(val); } + @DynamoDBFlattened(attributes={ + @DynamoDBAttribute(mappedBy="amount", attributeName="secondAmount"), + @DynamoDBAttribute(mappedBy="unit", attributeName="secondUnit")}) + public Currency getOther() { return this.other; } + public void setOther(final Currency other) { this.other = other; } + }; + final DynamoDBMapperTableModel model = getTable(obj); + assertEquals(5, model.fields().size()); + assertEquals("firstAmount", model.field("firstAmount").name()); + assertEquals("firstUnit", model.field("firstUnit").name()); + assertEquals("secondAmount", model.field("secondAmount").name()); + assertEquals("secondUnit", model.field("secondUnit").name()); + } + + /** + * Test mappings. + */ + @Test + public void testTableAndDocument() { + models.getTable(TableAndDocument.class); + } + @DynamoDBDocument @DynamoDBTable(tableName="") + public static class TableAndDocument extends AutoKeyAndVal { + public String getVal() { return super.getVal(); } + public void setVal(final String val) { super.setVal(val); } + } + + /** + * Test mappings. + */ + @Test + public void testInheritedWithNoTable() { + final Object obj = new KeyAndVal() { + @DynamoDBHashKey(attributeName="hk") + public String getKey() { return super.getKey(); } + public void setKey(String key) { super.setKey(key); } + @DynamoDBAttribute(attributeName="value") + public String getVal() { return super.getVal(); } + public void setVal(String val) { super.setVal(val); } + }; + final DynamoDBMapperTableModel model = getTable(obj); + + final DynamoDBMapperFieldModel key = model.field("hk"); + assertNotNull(key); + assertEquals(KeyType.HASH, key.keyType()); + assertEquals(DynamoDBAttributeType.S, key.attributeType()); + + final DynamoDBMapperFieldModel val = model.field("value"); + assertNotNull(val); + assertEquals(DynamoDBAttributeType.S, val.attributeType()); + } + + /** + * Test mappings to make sure the bridge method is ruled out. + */ + @Test + public void testFindRelevantGettersWithBridgeMethod() { + final DynamoDBMapperTableModel model = models.getTable(SubClass.class); + assertEquals("only two getter should be returned", 2, model.fields().size()); + assertEquals("return type should be Integer rather than Object", DynamoDBAttributeType.N, model.field("t").attributeType()); + } + @DynamoDBTable(tableName = "") + private static abstract class SuperGenericClass { + private String id; + + @DynamoDBHashKey + public final String getId() { return this.id; } + public final void setId(String id) { this.id = id; } + + public abstract T getT(); + public abstract void setT(T t); + } + @DynamoDBTable(tableName = "GenericString") + private static class SubClass extends SuperGenericClass { + private Integer t; + @Override + public Integer getT() { return t; } + @Override + public void setT(Integer t) { this.t = t; } + } + + /** + * Test mappings. + */ + @Test + public void testNonMappedInheritedProperties() { + final DynamoDBMapperTableModel model = models.getTable(NonMappedInheritedProperties.class); + assertEquals(2, model.fields().size()); + assertNotNull(model.field("doUse")); + } + public abstract class AbstractNonMappedInheritedProperties { + private String doNotUse; + + public String getDoNotUse() { return this.doNotUse; } + public void setDoNotUse(final String doNotUse) { this.doNotUse = doNotUse; } + } + @DynamoDBTable(tableName="aws-java-sdk-test") + public class NonMappedInheritedProperties extends AbstractNonMappedInheritedProperties { + private String id; + private String doUse; + + @DynamoDBHashKey + public final String getId() { return this.id; } + public final void setId(String id) { this.id = id; } + + public String getDoUse() { return this.doUse; } + public void setDoUse(final String doUse) { this.doUse = doUse; } + } + + /** + * Test mappings. + */ + @Test + public void testInheritedProperties() { + final DynamoDBMapperTableModel model1 = models.getTable(BaseTablePojo.class); + assertEquals(3, model1.fields().size()); + assertNotNull(model1.field("hashKeyOnField")); + assertNotNull(model1.field("rangeKeyOnGetter")); + final DynamoDBMapperTableModel model2 = models.getTable(TablePojoSubclass.class); + assertEquals(4, model2.fields().size()); + assertNotNull(model2.field("hashKeyOnField")); + assertNotNull(model2.field("rangeKeyOnGetter")); + } + @DynamoDBTable(tableName="table") + private static class BaseTablePojo { + @DynamoDBHashKey + private String hashKeyOnField; + private String rangeKeyOnGetter; + private String attrNoAnnotation; + @DynamoDBIgnore + private String ignoredAttr; + public String getHashKeyOnField() { return hashKeyOnField; } + public void setHashKeyOnField(String hashKeyOnField) { this.hashKeyOnField = hashKeyOnField; } + @DynamoDBRangeKey + public String getRangeKeyOnGetter() { return rangeKeyOnGetter; } + public void setRangeKeyOnGetter(String rangeKeyOnGetter) { this.rangeKeyOnGetter = rangeKeyOnGetter; } + public String getAttrNoAnnotation() { return attrNoAnnotation; } + public void setAttrNoAnnotation(String attrNoAnnotation) { this.attrNoAnnotation = attrNoAnnotation; } + public String getIgnoredAttr() { return ignoredAttr; } + public void setIgnoredAttr(String ignoredAttr) { this.ignoredAttr = ignoredAttr; } + } + @DynamoDBTable(tableName="table") + private static class TablePojoSubclass extends BaseTablePojo { + private String ignoredAttr; + @Override + public String getIgnoredAttr() { return ignoredAttr; } + @Override + public void setIgnoredAttr(String ignoredAttr) { this.ignoredAttr = ignoredAttr; } + } + + /** + * Test mappings. + */ + @Test + public void testPojoWithGetterAnnotations() { + PojoAsserts.assertAll(models.getTable(PojoWithGetterAnnotations.class)); + } + + /** + * Test mappings. + */ + @Test + public void testPojoWithFieldAnnotations() { + PojoAsserts.assertAll(models.getTable(PojoWithFieldAnnotations.class)); + } + + /** + * Test mappings. + */ + @Test + public void testPojoWithMixedAnnotations() { + PojoAsserts.assertAll(models.getTable(PojoWithMixedAnnotations.class)); + } + + /** + * Pojo field assersions. + */ + private static enum PojoAsserts { + hashKey(KeyType.HASH, null), + rangeKey(KeyType.RANGE, DynamoDBAutoGenerateStrategy.CREATE), + indexHashKey(null, null), + indexRangeKey(null, null), + actualAttrName(null, null), + versionedAttr(null, DynamoDBAutoGenerateStrategy.ALWAYS), + marshallingAttr(null, null); + private final DynamoDBAutoGenerateStrategy generateStrategy; + private final KeyType keyType; + private PojoAsserts(final KeyType keyType, final DynamoDBAutoGenerateStrategy generateStrategy) { + this.generateStrategy = generateStrategy; + this.keyType = keyType; + } + public static void assertAll(final DynamoDBMapperTableModel model) { + for (final PojoAsserts asserts : PojoAsserts.values()) { + final DynamoDBMapperFieldModel field = model.field(asserts.name()); + assertNotNull(field); + assertFieldKeyType(asserts.keyType, field, model); + assertEquals(asserts.generateStrategy, field.getGenerateStrategy()); + assertEquals(0, field.localSecondaryIndexNames().size()); + } + assertEquals(PojoAsserts.values().length, model.fields().size()); + } + } + + /** + * A POJO model that uses getter annotations. + */ + @DynamoDBTable(tableName="table") + private static class PojoWithGetterAnnotations { + private String hashKey; + private String rangeKey; + private String indexHashKey; + private String indexRangeKey; + private String annotatedAttr; + private Long versionedAttr; + private String marshallingAttr; + private String ignoredAttr; + @DynamoDBHashKey + public String getHashKey() { return hashKey; } + public void setHashKey(String hashKey) { this.hashKey = hashKey; } + @DynamoDBRangeKey @DynamoDBAutoGeneratedKey + public String getRangeKey() { return rangeKey; } + public void setRangeKey(String rangeKey) { this.rangeKey = rangeKey; } + @DynamoDBIndexHashKey(globalSecondaryIndexName="index") + public String getIndexHashKey() { return indexHashKey; } + public void setIndexHashKey(String indexHashKey) { this.indexHashKey = indexHashKey; } + @DynamoDBIndexRangeKey(globalSecondaryIndexName="index") + public String getIndexRangeKey() { return indexRangeKey; } + public void setIndexRangeKey(String indexRangeKey) { this.indexRangeKey = indexRangeKey; } + @DynamoDBAttribute(attributeName="actualAttrName") + public String getAnnotatedAttr() { return annotatedAttr; } + public void setAnnotatedAttr(String annotatedAttr) { this.annotatedAttr = annotatedAttr; } + @DynamoDBVersionAttribute + public Long getVersionedAttr() { return versionedAttr; } + public void setVersionedAttr(Long versionedAttr) { this.versionedAttr = versionedAttr; } + @DynamoDBTypeConverted(converter=RandomUUIDMarshaller.class) + public String getMarshallingAttr() { return marshallingAttr; } + public void setMarshallingAttr(String marshallingAttr) { this.marshallingAttr = marshallingAttr; } + @DynamoDBIgnore + public String getIgnoredAttr() { return ignoredAttr; } + public void setIgnoredAttr(String ignoredAttr) { this.ignoredAttr = ignoredAttr; } + } + + /** + * The same model as defined in PojoWithGetterAnnotations, but uses field + * annotations instead. + */ + @DynamoDBTable(tableName="table") + private static class PojoWithFieldAnnotations { + @DynamoDBHashKey + private String hashKey; + @DynamoDBRangeKey @DynamoDBAutoGeneratedKey + private String rangeKey; + @DynamoDBIndexHashKey(globalSecondaryIndexName="index") + private String indexHashKey; + @DynamoDBIndexRangeKey(globalSecondaryIndexName="index") + private String indexRangeKey; + @DynamoDBAttribute(attributeName="actualAttrName") + private String annotatedAttr; + @DynamoDBVersionAttribute + private Long versionedAttr; + @DynamoDBTypeConverted(converter=RandomUUIDMarshaller.class) + private String marshallingAttr; + @DynamoDBIgnore + private String ignoredAttr; + public String getHashKey() { return hashKey; } + public void setHashKey(String hashKey) { this.hashKey = hashKey; } + public String getRangeKey() { return rangeKey; } + public void setRangeKey(String rangeKey) { this.rangeKey = rangeKey; } + public String getIndexHashKey() { return indexHashKey; } + public void setIndexHashKey(String indexHashKey) { this.indexHashKey = indexHashKey; } + public String getIndexRangeKey() { return indexRangeKey; } + public void setIndexRangeKey(String indexRangeKey) { this.indexRangeKey = indexRangeKey; } + public String getAnnotatedAttr() { return annotatedAttr; } + public void setAnnotatedAttr(String annotatedAttr) { this.annotatedAttr = annotatedAttr; } + public Long getVersionedAttr() { return versionedAttr; } + public void setVersionedAttr(Long versionedAttr) { this.versionedAttr = versionedAttr; } + public String getMarshallingAttr() { return marshallingAttr; } + public void setMarshallingAttr(String marshallingAttr) { this.marshallingAttr = marshallingAttr; } + public String getIgnoredAttr() { return ignoredAttr; } + public void setIgnoredAttr(String ignoredAttr) { this.ignoredAttr = ignoredAttr; } + } + + /** + * The same model as defined in PojoWithGetterAnnotations, but uses both getter and field + * annotations. + */ + @DynamoDBTable(tableName="table") + private static class PojoWithMixedAnnotations { + @DynamoDBHashKey + private String hashKey; + private String rangeKey; + @DynamoDBIndexHashKey(globalSecondaryIndexName="index") + private String indexHashKey; + private String indexRangeKey; + @DynamoDBAttribute(attributeName="actualAttrName") + private String annotatedAttr; + private Long versionedAttr; + @DynamoDBTypeConverted(converter=RandomUUIDMarshaller.class) + private String marshallingAttr; + private String ignoredAttr; + public String getHashKey() { return hashKey; } + public void setHashKey(String hashKey) { this.hashKey = hashKey; } + @DynamoDBRangeKey @DynamoDBAutoGeneratedKey + public String getRangeKey() { return rangeKey; } + public void setRangeKey(String rangeKey) { this.rangeKey = rangeKey; } + public String getIndexHashKey() { return indexHashKey; } + public void setIndexHashKey(String indexHashKey) { this.indexHashKey = indexHashKey; } + @DynamoDBIndexRangeKey(globalSecondaryIndexName="index") + public String getIndexRangeKey() { return indexRangeKey; } + public void setIndexRangeKey(String indexRangeKey) { this.indexRangeKey = indexRangeKey; } + public String getAnnotatedAttr() { return annotatedAttr; } + public void setAnnotatedAttr(String annotatedAttr) { this.annotatedAttr = annotatedAttr; } + @DynamoDBVersionAttribute + public Long getVersionedAttr() { return versionedAttr; } + public void setVersionedAttr(Long versionedAttr) { this.versionedAttr = versionedAttr; } + public String getMarshallingAttr() { return marshallingAttr; } + public void setMarshallingAttr(String marshallingAttr) { this.marshallingAttr = marshallingAttr; } + @DynamoDBIgnore + public String getIgnoredAttr() { return ignoredAttr; } + public void setIgnoredAttr(String ignoredAttr) { this.ignoredAttr = ignoredAttr; } + } + + /** + * Assert that the field key properties are correct. + */ + private static void assertFieldKeyType(KeyType keyType, DynamoDBMapperFieldModel field, DynamoDBMapperTableModel model) { + assertEquals(keyType, field.keyType()); + if (keyType != null) { + if (keyType == KeyType.HASH) { + assertEquals(field, model.hashKey()); + } else if (keyType == KeyType.RANGE) { + assertEquals(field, model.rangeKeyIfExists()); + assertEquals(field, model.rangeKey()); + } + } + } + + /** + * Assert that the field contains the LSIs. + */ + private static void assertFieldGsiNames(List names, KeyType keyType, DynamoDBMapperFieldModel field, DynamoDBMapperTableModel model) { + assertEquals(names == null ? 0 : names.size(), field.globalSecondaryIndexNames(keyType).size()); + assertEquals(true, field.indexed()); + if (names != null) { + for (final String name : names) { + assertEquals(true, field.globalSecondaryIndexNames(keyType).contains(name)); + assertEquals(true, model.globalSecondaryIndex(name) != null); + assertEquals(true, !model.globalSecondaryIndexes().isEmpty()); + } + } + } + + /** + * Assert that the field contains the LSIs. + */ + private static void assertFieldLsiNames(List names, DynamoDBMapperFieldModel field, DynamoDBMapperTableModel model) { + assertEquals(names == null ? 0 : names.size(), field.localSecondaryIndexNames().size()); + assertEquals(true, field.indexed()); + if (names != null) { + for (final String name : names) { + assertEquals(true, field.localSecondaryIndexNames().contains(name)); + assertEquals(true, model.localSecondaryIndex(name) != null); + assertEquals(true, !model.localSecondaryIndexes().isEmpty()); + } + } + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/StandardModelFactoriesV1Test.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/StandardModelFactoriesV1Test.java new file mode 100644 index 000000000000..fc6e35e17e8d --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/StandardModelFactoriesV1Test.java @@ -0,0 +1,333 @@ +/* + * 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.mapper.dynamodb; +import software.amazon.awssdk.mapper.dynamodb.internal.DynamoDBMapperModelFactory; + +import static org.junit.Assert.assertEquals; + +import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.LinkedHashSet; +import java.util.TreeSet; +import java.util.UUID; + +import org.junit.Assert; +import org.junit.Test; + +import com.amazonaws.auth.AWSCredentialsProvider; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.mapper.dynamodb.pojos.SubClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.TestClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.UnannotatedSubClass; + +public class StandardModelFactoriesV1Test { + + protected static final DynamoDBMapperConfig CONFIG = new DynamoDBMapperConfig.Builder() + .withTypeConverterFactory(DynamoDBMapperConfig.DEFAULT.getTypeConverterFactory()) + .withConversionSchema(ConversionSchemas.V1) + .build(); + + private static final DynamoDBMapperModelFactory factory = StandardModelFactories.of(S3Link.Factory.of(null)); + private static final DynamoDBMapperModelFactory.TableFactory models = factory.getTableFactory(CONFIG); + + protected AttributeValue convert(Class clazz, Method getter, Object value) { + final StandardAnnotationMaps.FieldMap map = StandardAnnotationMaps.of(getter, null); + return models.getTable(clazz).field(map.attributeName()).convert(value); + } + + @Test + public void testBoolean() { + assertEquals("1", convert("getBoolean", true).n()); + assertEquals("0", convert("getBoolean", false).n()); + assertEquals("1", convert("getBoxedBoolean", true).n()); + assertEquals("0", convert("getBoxedBoolean", false).n()); + + assertEquals(true, convert("getNativeBoolean", true).bool()); + assertEquals(false, convert("getNativeBoolean", false).bool()); + } + + @Test + public void testString() { + assertEquals("abc", convert("getString", "abc").s()); + + assertEquals(RandomUUIDMarshaller.randomUUID, + convert("getCustomString", "abc").s()); + } + + @Test + public void testUuid() { + UUID uuid = UUID.randomUUID(); + assertEquals(uuid.toString(), convert("getUuid", uuid).s()); + } + + @Test + public void testDate() { + assertEquals("1970-01-01T00:00:00.000Z", + convert("getDate", new Date(0)).s()); + + Calendar c = GregorianCalendar.getInstance(); + c.setTimeInMillis(0); + + assertEquals("1970-01-01T00:00:00.000Z", + convert("getCalendar", c).s()); + } + + @Test + public void testNumbers() { + assertEquals("0", convert("getByte", (byte) 0).n()); + assertEquals("1", convert("getByte", (byte) 1).n()); + assertEquals("0", convert("getBoxedByte", (byte) 0).n()); + assertEquals("1", convert("getBoxedByte", (byte) 1).n()); + + assertEquals("0", convert("getShort", (short) 0).n()); + assertEquals("1", convert("getShort", (short) 1).n()); + assertEquals("0", convert("getBoxedShort", (short) 0).n()); + assertEquals("1", convert("getBoxedShort", (short) 1).n()); + + assertEquals("0", convert("getInt", 0).n()); + assertEquals("1", convert("getInt", 1).n()); + assertEquals("0", convert("getBoxedInt", 0).n()); + assertEquals("1", convert("getBoxedInt", 1).n()); + + assertEquals("0", convert("getLong", 0l).n()); + assertEquals("1", convert("getLong", 1l).n()); + assertEquals("0", convert("getBoxedLong", 0l).n()); + assertEquals("1", convert("getBoxedLong", 1l).n()); + + assertEquals("0", convert("getBigInt", BigInteger.ZERO).n()); + assertEquals("1", convert("getBigInt", BigInteger.ONE).n()); + + assertEquals("0.0", convert("getFloat", 0f).n()); + assertEquals("1.0", convert("getFloat", 1f).n()); + assertEquals("0.0", convert("getBoxedFloat", 0f).n()); + assertEquals("1.0", convert("getBoxedFloat", 1f).n()); + + assertEquals("0.0", convert("getDouble", 0d).n()); + assertEquals("1.0", convert("getDouble", 1d).n()); + assertEquals("0.0", convert("getBoxedDouble", 0d).n()); + assertEquals("1.0", convert("getBoxedDouble", 1d).n()); + + assertEquals("0", convert("getBigDecimal", BigDecimal.ZERO).n()); + assertEquals("1", convert("getBigDecimal", BigDecimal.ONE).n()); + } + + @Test + public void testBinary() { + ByteBuffer value = ByteBuffer.wrap("value".getBytes()); + assertEquals(value.slice(), convert("getByteArray", "value".getBytes()).b().asByteBuffer()); + assertEquals(value.slice(), convert("getByteBuffer", value.slice()).b().asByteBuffer()); + } + + @Test + public void testBooleanSet() { + assertEquals(Collections.singletonList("1"), + convert("getBooleanSet", Collections.singleton(true)).ns()); + + assertEquals(Collections.singletonList("0"), + convert("getBooleanSet", Collections.singleton(false)).ns()); + + assertEquals(Arrays.asList("0", "1"), + convert("getBooleanSet", new TreeSet() {{ + add(true); + add(false); + }}).ns()); + } + + @Test + public void testStringSet() { + assertEquals(Collections.singletonList("a"), + convert("getStringSet", Collections.singleton("a")).ss()); + assertEquals(Collections.singletonList("b"), + convert("getStringSet", Collections.singleton("b")).ss()); + + assertEquals(Arrays.asList("a", "b", "c"), + convert("getStringSet", new TreeSet() {{ + add("a"); + add("b"); + add("c"); + }}).ss()); + } + + @Test + public void testUuidSet() { + final UUID one = UUID.randomUUID(); + final UUID two = UUID.randomUUID(); + final UUID three = UUID.randomUUID(); + + assertEquals(Collections.singletonList(one.toString()), + convert("getUuidSet", Collections.singleton(one)).ss()); + + assertEquals(Collections.singletonList(two.toString()), + convert("getUuidSet", Collections.singleton(two)).ss()); + + assertEquals( + Arrays.asList( + one.toString(), + two.toString(), + three.toString()), + convert("getUuidSet", new LinkedHashSet() {{ + add(one); + add(two); + add(three); + }}).ss()); + } + + @Test + public void testDateSet() { + assertEquals(Collections.singletonList("1970-01-01T00:00:00.000Z"), + convert("getDateSet", Collections.singleton(new Date(0))) + .ss()); + + Calendar c = GregorianCalendar.getInstance(); + c.setTimeInMillis(0); + + assertEquals(Collections.singletonList("1970-01-01T00:00:00.000Z"), + convert("getCalendarSet", Collections.singleton(c)) + .ss()); + } + + @Test + public void testNumberSet() { + assertEquals(Collections.singletonList("0"), + convert("getByteSet", Collections.singleton((byte) 0)).ns()); + assertEquals(Collections.singletonList("0"), + convert("getShortSet", Collections.singleton((short) 0)).ns()); + assertEquals(Collections.singletonList("0"), + convert("getIntSet", Collections.singleton(0)).ns()); + assertEquals(Collections.singletonList("0"), + convert("getLongSet", Collections.singleton(0l)).ns()); + assertEquals(Collections.singletonList("0"), + convert("getBigIntegerSet", Collections.singleton(BigInteger.ZERO)) + .ns()); + assertEquals(Collections.singletonList("0.0"), + convert("getFloatSet", Collections.singleton(0f)).ns()); + assertEquals(Collections.singletonList("0.0"), + convert("getDoubleSet", Collections.singleton(0d)).ns()); + assertEquals(Collections.singletonList("0"), + convert("getBigDecimalSet", Collections.singleton(BigDecimal.ZERO)) + .ns()); + + assertEquals(Arrays.asList("0", "1", "2"), + convert("getLongSet", new TreeSet() {{ + add(0); + add(1); + add(2); + }}).ns()); + } + + @Test + public void testBinarySet() { + final ByteBuffer test = ByteBuffer.wrap("test".getBytes()); + final ByteBuffer test2 = ByteBuffer.wrap("test2".getBytes()); + + assertEquals(Collections.singletonList(SdkBytes.fromByteBuffer(test.slice())), + convert("getByteArraySet", Collections.singleton("test".getBytes())) + .bs()); + + assertEquals(Collections.singletonList(SdkBytes.fromByteBuffer(test.slice())), + convert("getByteBufferSet", Collections.singleton(test.slice())) + .bs()); + + assertEquals(Arrays.asList(SdkBytes.fromByteBuffer(test.slice()), SdkBytes.fromByteBuffer(test2.slice())), + convert("getByteBufferSet",new TreeSet() {{ + add(test.slice()); + add(test2.slice()); + }}).bs()); + } + + @Test + public void testObjectSet() { + Object o = new Object() { + @Override + public String toString() { + return "hello"; + } + }; + + assertEquals(Collections.singletonList("hello"), + convert("getObjectSet", Collections.singleton(o)).ss()); + } + + @Test + public void testList() { + try { + convert("getList", Arrays.asList("a", "b", "c")); + Assert.fail("Expected DynamoDBMappingException"); + } catch (DynamoDBMappingException e) { + } + } + + @Test + public void testMap() { + try { + convert("getMap", Collections.singletonMap("a", "b")); + Assert.fail("Expected DynamoDBMappingException"); + } catch (DynamoDBMappingException e) { + } + } + + @Test + public void testObject() { + try { + convert("getObject", new SubClass()); + Assert.fail("Expected DynamoDBMappingException"); + } catch (DynamoDBMappingException e) { + } + } + + @Test + public void testUnannotatedObject() throws Exception { + try { + convert(UnannotatedSubClass.class, UnannotatedSubClass.class.getMethod("getChild"), + new UnannotatedSubClass()); + + Assert.fail("Expected DynamoDBMappingException"); + } catch (DynamoDBMappingException e) { + } + } + + @Test + public void testS3Link() { + S3ClientCache cache = new S3ClientCache((AWSCredentialsProvider) null); + S3Link link = new S3Link(cache, "bucket", "key"); + + assertEquals("{\"s3\":{" + + "\"bucket\":\"bucket\"," + + "\"key\":\"key\"," + + "\"region\":null}}", + convert("getS3Link", link).s()); + } + + private AttributeValue convert(String getter, Object value) { + try { + return convert(TestClass.class, TestClass.class.getMethod(getter), value); + + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/StandardModelFactoriesV2CompatibleTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/StandardModelFactoriesV2CompatibleTest.java new file mode 100644 index 000000000000..20ae202c0311 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/StandardModelFactoriesV2CompatibleTest.java @@ -0,0 +1,378 @@ +/* + * 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.mapper.dynamodb; +import software.amazon.awssdk.mapper.dynamodb.internal.DynamoDBMapperModelFactory; + +import static org.junit.Assert.assertEquals; + +import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.TreeSet; +import java.util.UUID; + +import org.junit.Assert; +import org.junit.Test; + +import com.amazonaws.auth.AWSCredentialsProvider; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.mapper.dynamodb.pojos.SubClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.TestClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.UnannotatedSubClass; + +public class StandardModelFactoriesV2CompatibleTest { + + protected static final DynamoDBMapperConfig CONFIG = new DynamoDBMapperConfig.Builder() + .withTypeConverterFactory(DynamoDBMapperConfig.DEFAULT.getTypeConverterFactory()) + .withConversionSchema(ConversionSchemas.V2_COMPATIBLE) + .build(); + + private static final DynamoDBMapperModelFactory factory = StandardModelFactories.of(S3Link.Factory.of(null)); + private static final DynamoDBMapperModelFactory.TableFactory models = factory.getTableFactory(CONFIG); + + protected AttributeValue convert(Class clazz, Method getter, Object value) { + final StandardAnnotationMaps.FieldMap map = StandardAnnotationMaps.of(getter, null); + return models.getTable(clazz).field(map.attributeName()).convert(value); + } + + @Test + public void testBoolean() { + assertEquals("1", convert("getBoolean", true).n()); + assertEquals("0", convert("getBoolean", false).n()); + assertEquals("1", convert("getBoxedBoolean", true).n()); + assertEquals("0", convert("getBoxedBoolean", false).n()); + + assertEquals(true, convert("getNativeBoolean", true).bool()); + assertEquals(false, convert("getNativeBoolean", false).bool()); + } + + @Test + public void testString() { + assertEquals("abc", convert("getString", "abc").s()); + + assertEquals(RandomUUIDMarshaller.randomUUID, + convert("getCustomString", "abc").s()); + } + + @Test + public void testUuid() { + UUID uuid = UUID.randomUUID(); + assertEquals(uuid.toString(), convert("getUuid", uuid).s()); + } + + @Test + public void testDate() { + assertEquals("1970-01-01T00:00:00.000Z", + convert("getDate", new Date(0)).s()); + + Calendar c = GregorianCalendar.getInstance(); + c.setTimeInMillis(0); + + assertEquals("1970-01-01T00:00:00.000Z", + convert("getCalendar", c).s()); + } + + @Test + public void testNumbers() { + assertEquals("0", convert("getByte", (byte) 0).n()); + assertEquals("1", convert("getByte", (byte) 1).n()); + assertEquals("0", convert("getBoxedByte", (byte) 0).n()); + assertEquals("1", convert("getBoxedByte", (byte) 1).n()); + + assertEquals("0", convert("getShort", (short) 0).n()); + assertEquals("1", convert("getShort", (short) 1).n()); + assertEquals("0", convert("getBoxedShort", (short) 0).n()); + assertEquals("1", convert("getBoxedShort", (short) 1).n()); + + assertEquals("0", convert("getInt", 0).n()); + assertEquals("1", convert("getInt", 1).n()); + assertEquals("0", convert("getBoxedInt", 0).n()); + assertEquals("1", convert("getBoxedInt", 1).n()); + + assertEquals("0", convert("getLong", 0l).n()); + assertEquals("1", convert("getLong", 1l).n()); + assertEquals("0", convert("getBoxedLong", 0l).n()); + assertEquals("1", convert("getBoxedLong", 1l).n()); + + assertEquals("0", convert("getBigInt", BigInteger.ZERO).n()); + assertEquals("1", convert("getBigInt", BigInteger.ONE).n()); + + assertEquals("0.0", convert("getFloat", 0f).n()); + assertEquals("1.0", convert("getFloat", 1f).n()); + assertEquals("0.0", convert("getBoxedFloat", 0f).n()); + assertEquals("1.0", convert("getBoxedFloat", 1f).n()); + + assertEquals("0.0", convert("getDouble", 0d).n()); + assertEquals("1.0", convert("getDouble", 1d).n()); + assertEquals("0.0", convert("getBoxedDouble", 0d).n()); + assertEquals("1.0", convert("getBoxedDouble", 1d).n()); + + assertEquals("0", convert("getBigDecimal", BigDecimal.ZERO).n()); + assertEquals("1", convert("getBigDecimal", BigDecimal.ONE).n()); + } + + @Test + public void testBinary() { + ByteBuffer value = ByteBuffer.wrap("value".getBytes()); + assertEquals(value.slice(), convert("getByteArray", "value".getBytes()).b().asByteBuffer()); + assertEquals(value.slice(), convert("getByteBuffer", value.slice()).b().asByteBuffer()); + } + + @Test + public void testBooleanSet() { + assertEquals(Collections.singletonList("1"), + convert("getBooleanSet", Collections.singleton(true)).ns()); + + assertEquals(Collections.singletonList("0"), + convert("getBooleanSet", Collections.singleton(false)).ns()); + + assertEquals(Arrays.asList("0", "1"), + convert("getBooleanSet", new TreeSet() {{ + add(true); + add(false); + }}).ns()); + } + + @Test + public void testStringSet() { + assertEquals(Collections.singletonList("a"), + convert("getStringSet", Collections.singleton("a")).ss()); + assertEquals(Collections.singletonList("b"), + convert("getStringSet", Collections.singleton("b")).ss()); + + assertEquals(Arrays.asList("a", "b", "c"), + convert("getStringSet", new TreeSet() {{ + add("a"); + add("b"); + add("c"); + }}).ss()); + } + + @Test + public void testUuidSet() { + final UUID one = UUID.randomUUID(); + final UUID two = UUID.randomUUID(); + final UUID three = UUID.randomUUID(); + + assertEquals(Collections.singletonList(one.toString()), + convert("getUuidSet", Collections.singleton(one)).ss()); + + assertEquals(Collections.singletonList(two.toString()), + convert("getUuidSet", Collections.singleton(two)).ss()); + + assertEquals( + Arrays.asList( + one.toString(), + two.toString(), + three.toString()), + convert("getUuidSet", new LinkedHashSet() {{ + add(one); + add(two); + add(three); + }}).ss()); + } + + @Test + public void testDateSet() { + assertEquals(Collections.singletonList("1970-01-01T00:00:00.000Z"), + convert("getDateSet", Collections.singleton(new Date(0))) + .ss()); + + Calendar c = GregorianCalendar.getInstance(); + c.setTimeInMillis(0); + + assertEquals(Collections.singletonList("1970-01-01T00:00:00.000Z"), + convert("getCalendarSet", Collections.singleton(c)) + .ss()); + } + + @Test + public void testNumberSet() { + assertEquals(Collections.singletonList("0"), + convert("getByteSet", Collections.singleton((byte) 0)).ns()); + assertEquals(Collections.singletonList("0"), + convert("getShortSet", Collections.singleton((short) 0)).ns()); + assertEquals(Collections.singletonList("0"), + convert("getIntSet", Collections.singleton(0)).ns()); + assertEquals(Collections.singletonList("0"), + convert("getLongSet", Collections.singleton(0l)).ns()); + assertEquals(Collections.singletonList("0"), + convert("getBigIntegerSet", Collections.singleton(BigInteger.ZERO)) + .ns()); + assertEquals(Collections.singletonList("0.0"), + convert("getFloatSet", Collections.singleton(0f)).ns()); + assertEquals(Collections.singletonList("0.0"), + convert("getDoubleSet", Collections.singleton(0d)).ns()); + assertEquals(Collections.singletonList("0"), + convert("getBigDecimalSet", Collections.singleton(BigDecimal.ZERO)) + .ns()); + + assertEquals(Arrays.asList("0", "1", "2"), + convert("getLongSet", new TreeSet() {{ + add(0); + add(1); + add(2); + }}).ns()); + } + + @Test + public void testBinarySet() { + final ByteBuffer test = ByteBuffer.wrap("test".getBytes()); + final ByteBuffer test2 = ByteBuffer.wrap("test2".getBytes()); + + assertEquals(Collections.singletonList(SdkBytes.fromByteBuffer(test.slice())), + convert("getByteArraySet", Collections.singleton("test".getBytes())) + .bs()); + + assertEquals(Collections.singletonList(SdkBytes.fromByteBuffer(test.slice())), + convert("getByteBufferSet", Collections.singleton(test.slice())) + .bs()); + + assertEquals(Arrays.asList(SdkBytes.fromByteBuffer(test.slice()), SdkBytes.fromByteBuffer(test2.slice())), + convert("getByteBufferSet",new TreeSet() {{ + add(test.slice()); + add(test2.slice()); + }}).bs()); + } + + @Test + public void testObjectSet() { + Object o = new Object() { + @Override + public String toString() { + return "hello"; + } + }; + + assertEquals(Collections.singletonList("hello"), + convert("getObjectSet", Collections.singleton(o)).ss()); + } + + @Test + public void testList() { + assertEquals(Arrays.asList( + AttributeValue.builder().s("a").build(), + AttributeValue.builder().s("b").build(), + AttributeValue.builder().s("c").build()), + convert("getList", Arrays.asList("a", "b", "c")).l()); + + assertEquals(Arrays.asList(AttributeValue.builder().nul(true).build()), + convert("getList", Collections.singletonList(null)).l()); + } + + @Test + public void testSetList() { + assertEquals(Arrays.asList( + AttributeValue.builder().ss("a").build(), + AttributeValue.builder().ss("b").build(), + AttributeValue.builder().ss("c").build()), + convert("getSetList", Arrays.asList( + Collections.singleton("a"), + Collections.singleton("b"), + Collections.singleton("c"))).l()); + } + + @Test + public void testMap() { + assertEquals(new HashMap() {{ + put("a", AttributeValue.builder().s("b").build()); + put("c", AttributeValue.builder().s("d").build()); + put("e", AttributeValue.builder().s("f").build()); + }}, + convert("getMap", new HashMap() {{ + put("a", "b"); + put("c", "d"); + put("e", "f"); + }}).m()); + + assertEquals(Collections.singletonMap("a", AttributeValue.builder().nul(true).build()), + convert("getMap", Collections.singletonMap("a", null)).m()); + } + + @Test + public void testSetMap() { + assertEquals(new HashMap() {{ + put("a", AttributeValue.builder().ss("a", "b").build()); + }}, + convert("getSetMap", new HashMap>() {{ + put("a", new TreeSet(Arrays.asList("a", "b"))); + }}).m()); + + assertEquals(new HashMap() {{ + put("a", AttributeValue.builder().ss("a").build()); + put("b", AttributeValue.builder().nul(true).build()); + }}, + convert("getSetMap", new HashMap>() {{ + put("a", new TreeSet(Arrays.asList("a"))); + put("b", null); + }}).m()); + } + + @Test + public void testObject() { + assertEquals(new HashMap() {{ + put("name", AttributeValue.builder().s("name").build()); + put("value", AttributeValue.builder().n("123").build()); + }}, + convert("getObject", new SubClass()).m()); + } + + @Test + public void testUnannotatedObject() throws Exception { + try { + convert(UnannotatedSubClass.class, UnannotatedSubClass.class.getMethod("getChild"), + new UnannotatedSubClass()); + + Assert.fail("Expected DynamoDBMappingException"); + } catch (DynamoDBMappingException e) { + } + } + + @Test + public void testS3Link() { + S3ClientCache cache = new S3ClientCache((AWSCredentialsProvider) null); + S3Link link = new S3Link(cache, "bucket", "key"); + + assertEquals("{\"s3\":{" + + "\"bucket\":\"bucket\"," + + "\"key\":\"key\"," + + "\"region\":null}}", + convert("getS3Link", link).s()); + } + + private AttributeValue convert(String getter, Object value) { + try { + + return convert(TestClass.class, TestClass.class.getMethod(getter), value); + + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/StandardModelFactoriesV2Test.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/StandardModelFactoriesV2Test.java new file mode 100644 index 000000000000..f8e01725e53b --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/StandardModelFactoriesV2Test.java @@ -0,0 +1,396 @@ +/* + * 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.mapper.dynamodb; +import software.amazon.awssdk.mapper.dynamodb.internal.DynamoDBMapperModelFactory; + +import static org.junit.Assert.assertEquals; + +import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.UUID; + +import org.junit.Assert; +import org.junit.Test; + +import com.amazonaws.auth.AWSCredentialsProvider; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.mapper.dynamodb.pojos.SubClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.TestClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.UnannotatedSubClass; + +public class StandardModelFactoriesV2Test { + + protected static final DynamoDBMapperConfig CONFIG = new DynamoDBMapperConfig.Builder() + .withTypeConverterFactory(DynamoDBMapperConfig.DEFAULT.getTypeConverterFactory()) + .withConversionSchema(ConversionSchemas.V2) + .build(); + + private static final DynamoDBMapperModelFactory factory = StandardModelFactories.of(S3Link.Factory.of(null)); + private static final DynamoDBMapperModelFactory.TableFactory models = factory.getTableFactory(CONFIG); + + protected AttributeValue convert(Class clazz, Method getter, Object value) { + final StandardAnnotationMaps.FieldMap map = StandardAnnotationMaps.of(getter, null); + return models.getTable(clazz).field(map.attributeName()).convert(value); + } + + @Test + public void testBoolean() { + // These are all native booleans by default in the v2 schema + assertEquals(true, convert("getBoolean", true).bool()); + assertEquals(false, convert("getBoolean", false).bool()); + assertEquals(true, convert("getBoxedBoolean", true).bool()); + assertEquals(false, convert("getBoxedBoolean", false).bool()); + assertEquals(true, convert("getNativeBoolean", true).bool()); + assertEquals(false, convert("getNativeBoolean", false).bool()); + } + + @Test + public void testString() { + assertEquals("abc", convert("getString", "abc").s()); + + assertEquals(RandomUUIDMarshaller.randomUUID, + convert("getCustomString", "abc").s()); + } + + @Test + public void testUuid() { + UUID uuid = UUID.randomUUID(); + assertEquals(uuid.toString(), convert("getUuid", uuid).s()); + } + + @Test + public void testDate() { + assertEquals("1970-01-01T00:00:00.000Z", + convert("getDate", new Date(0)).s()); + + Calendar c = GregorianCalendar.getInstance(); + c.setTimeInMillis(0); + + assertEquals("1970-01-01T00:00:00.000Z", + convert("getCalendar", c).s()); + } + + @Test + public void testNumbers() { + assertEquals("0", convert("getByte", (byte) 0).n()); + assertEquals("1", convert("getByte", (byte) 1).n()); + assertEquals("0", convert("getBoxedByte", (byte) 0).n()); + assertEquals("1", convert("getBoxedByte", (byte) 1).n()); + + assertEquals("0", convert("getShort", (short) 0).n()); + assertEquals("1", convert("getShort", (short) 1).n()); + assertEquals("0", convert("getBoxedShort", (short) 0).n()); + assertEquals("1", convert("getBoxedShort", (short) 1).n()); + + assertEquals("0", convert("getInt", 0).n()); + assertEquals("1", convert("getInt", 1).n()); + assertEquals("0", convert("getBoxedInt", 0).n()); + assertEquals("1", convert("getBoxedInt", 1).n()); + + assertEquals("0", convert("getLong", 0l).n()); + assertEquals("1", convert("getLong", 1l).n()); + assertEquals("0", convert("getBoxedLong", 0l).n()); + assertEquals("1", convert("getBoxedLong", 1l).n()); + + assertEquals("0", convert("getBigInt", BigInteger.ZERO).n()); + assertEquals("1", convert("getBigInt", BigInteger.ONE).n()); + + assertEquals("0.0", convert("getFloat", 0f).n()); + assertEquals("1.0", convert("getFloat", 1f).n()); + assertEquals("0.0", convert("getBoxedFloat", 0f).n()); + assertEquals("1.0", convert("getBoxedFloat", 1f).n()); + + assertEquals("0.0", convert("getDouble", 0d).n()); + assertEquals("1.0", convert("getDouble", 1d).n()); + assertEquals("0.0", convert("getBoxedDouble", 0d).n()); + assertEquals("1.0", convert("getBoxedDouble", 1d).n()); + + assertEquals("0", convert("getBigDecimal", BigDecimal.ZERO).n()); + assertEquals("1", convert("getBigDecimal", BigDecimal.ONE).n()); + } + + @Test + public void testBinary() { + ByteBuffer value = ByteBuffer.wrap("value".getBytes()); + assertEquals(value.slice(), convert("getByteArray", "value".getBytes()).b().asByteBuffer()); + assertEquals(value.slice(), convert("getByteBuffer", value.slice()).b().asByteBuffer()); + } + + @Test + public void testBooleanSet() { + // Set (which is silly but technically valid) gets mapped to + // a List of Booleans now via the ObjectSetToListMarshaller. + AttributeValue value = + convert("getBooleanSet", Collections.singleton(true)); + + Assert.assertEquals(1, value.l().size()); + Assert.assertEquals(true, value.l().get(0).bool()); + } + + @Test + public void testStringSet() { + assertEquals(Collections.singletonList("a"), + convert("getStringSet", Collections.singleton("a")).ss()); + assertEquals(Collections.singletonList("b"), + convert("getStringSet", Collections.singleton("b")).ss()); + + assertEquals(Arrays.asList("a", "b", "c"), + convert("getStringSet", new TreeSet() {{ + add("a"); + add("b"); + add("c"); + }}).ss()); + } + + @Test + public void testUuidSet() { + final UUID one = UUID.randomUUID(); + final UUID two = UUID.randomUUID(); + final UUID three = UUID.randomUUID(); + + assertEquals(Collections.singletonList(one.toString()), + convert("getUuidSet", Collections.singleton(one)).ss()); + + assertEquals(Collections.singletonList(two.toString()), + convert("getUuidSet", Collections.singleton(two)).ss()); + + assertEquals( + Arrays.asList( + one.toString(), + two.toString(), + three.toString()), + convert("getUuidSet", new LinkedHashSet() {{ + add(one); + add(two); + add(three); + }}).ss()); + } + + @Test + public void testDateSet() { + assertEquals(Collections.singletonList("1970-01-01T00:00:00.000Z"), + convert("getDateSet", Collections.singleton(new Date(0))) + .ss()); + + Calendar c = GregorianCalendar.getInstance(); + c.setTimeInMillis(0); + + assertEquals(Collections.singletonList("1970-01-01T00:00:00.000Z"), + convert("getCalendarSet", Collections.singleton(c)) + .ss()); + } + + @Test + public void testNumberSet() { + assertEquals(Collections.singletonList("0"), + convert("getByteSet", Collections.singleton((byte) 0)).ns()); + assertEquals(Collections.singletonList("0"), + convert("getShortSet", Collections.singleton((short) 0)).ns()); + assertEquals(Collections.singletonList("0"), + convert("getIntSet", Collections.singleton(0)).ns()); + assertEquals(Collections.singletonList("0"), + convert("getLongSet", Collections.singleton(0l)).ns()); + assertEquals(Collections.singletonList("0"), + convert("getBigIntegerSet", Collections.singleton(BigInteger.ZERO)) + .ns()); + assertEquals(Collections.singletonList("0.0"), + convert("getFloatSet", Collections.singleton(0f)).ns()); + assertEquals(Collections.singletonList("0.0"), + convert("getDoubleSet", Collections.singleton(0d)).ns()); + assertEquals(Collections.singletonList("0"), + convert("getBigDecimalSet", Collections.singleton(BigDecimal.ZERO)) + .ns()); + + assertEquals(Arrays.asList("0", "1", "2"), + convert("getLongSet", new TreeSet() {{ + add(0); + add(1); + add(2); + }}).ns()); + } + + @Test + public void testBinarySet() { + final ByteBuffer test = ByteBuffer.wrap("test".getBytes()); + final ByteBuffer test2 = ByteBuffer.wrap("test2".getBytes()); + + assertEquals(Collections.singletonList(SdkBytes.fromByteBuffer(test.slice())), + convert("getByteArraySet", Collections.singleton("test".getBytes())) + .bs()); + + assertEquals(Collections.singletonList(SdkBytes.fromByteBuffer(test.slice())), + convert("getByteBufferSet", Collections.singleton(test.slice())) + .bs()); + + assertEquals(Arrays.asList(SdkBytes.fromByteBuffer(test.slice()), SdkBytes.fromByteBuffer(test2.slice())), + convert("getByteBufferSet",new TreeSet() {{ + add(test.slice()); + add(test2.slice()); + }}).bs()); + } + + @Test + public void testObjectSet() { + AttributeValue value = + convert("getObjectSet", Collections.singleton(new SubClass())); + + assertEquals(1, value.l().size()); + assertEquals(new HashMap() {{ + put("name", AttributeValue.builder().s("name").build()); + put("value", AttributeValue.builder().n("123").build()); + }}, + value.l().get(0).m()); + + assertEquals(Arrays.asList(AttributeValue.builder().nul(true).build()), + convert("getObjectSet", Collections.singleton(null)).l()); + } + + @Test + public void testList() { + assertEquals(Arrays.asList( + AttributeValue.builder().s("a").build(), + AttributeValue.builder().s("b").build(), + AttributeValue.builder().s("c").build()), + convert("getList", Arrays.asList("a", "b", "c")).l()); + + assertEquals(Arrays.asList(AttributeValue.builder().nul(true).build()), + convert("getList", Collections.singletonList(null)).l()); + } + + @Test + public void testObjectList() { + AttributeValue value = convert( + "getObjectList", + Collections.singletonList(new SubClass())); + + assertEquals(1, value.l().size()); + assertEquals(new HashMap() {{ + put("name", AttributeValue.builder().s("name").build()); + put("value", AttributeValue.builder().n("123").build()); + }}, + value.l().get(0).m()); + } + + @Test + public void testSetList() { + assertEquals( + Arrays.asList(AttributeValue.builder().ss("a").build()), + convert("getSetList", Arrays.asList( + Collections.singleton("a"))).l()); + + List> list = new ArrayList>(); + list.add(null); + + assertEquals( + Arrays.asList(AttributeValue.builder().nul(true).build()), + convert("getSetList", list).l()); + } + + @Test + public void testMap() { + assertEquals(new HashMap() {{ + put("a", AttributeValue.builder().s("b").build()); + put("c", AttributeValue.builder().s("d").build()); + put("e", AttributeValue.builder().s("f").build()); + }}, + convert("getMap", new HashMap() {{ + put("a", "b"); + put("c", "d"); + put("e", "f"); + }}).m()); + + assertEquals(Collections.singletonMap("a", AttributeValue.builder().nul(true).build()), + convert("getMap", Collections.singletonMap("a", null)).m()); + } + + @Test + public void testSetMap() { + assertEquals(new HashMap() {{ + put("a", AttributeValue.builder().ss("a", "b").build()); + }}, + convert("getSetMap", new HashMap>() {{ + put("a", new TreeSet(Arrays.asList("a", "b"))); + }}).m()); + + assertEquals(new HashMap() {{ + put("a", AttributeValue.builder().ss("a").build()); + put("b", AttributeValue.builder().nul(true).build()); + }}, + convert("getSetMap", new HashMap>() {{ + put("a", new TreeSet(Arrays.asList("a"))); + put("b", null); + }}).m()); + } + + @Test + public void testObject() { + assertEquals(new HashMap() {{ + put("name", AttributeValue.builder().s("name").build()); + put("value", AttributeValue.builder().n("123").build()); + }}, + convert("getObject", new SubClass()).m()); + } + + @Test + public void testUnannotatedObject() throws Exception { + try { + convert(UnannotatedSubClass.class, UnannotatedSubClass.class.getMethod("getChild"), + new UnannotatedSubClass()); + + Assert.fail("Expected DynamoDBMappingException"); + } catch (DynamoDBMappingException e) { + } + } + + @Test + public void testS3Link() { + S3ClientCache cache = new S3ClientCache((AWSCredentialsProvider) null); + S3Link link = new S3Link(cache, "bucket", "key"); + + assertEquals("{\"s3\":{" + + "\"bucket\":\"bucket\"," + + "\"key\":\"key\"," + + "\"region\":null}}", + convert("getS3Link", link).s()); + } + + private AttributeValue convert(String getter, Object value) { + try { + + return convert(TestClass.class, TestClass.class.getMethod(getter), value); + + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/StandardModelFactoriesV2UnconvertTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/StandardModelFactoriesV2UnconvertTest.java new file mode 100644 index 000000000000..0d4f4370ba00 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/StandardModelFactoriesV2UnconvertTest.java @@ -0,0 +1,485 @@ +/* + * 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.mapper.dynamodb; +import software.amazon.awssdk.mapper.dynamodb.internal.DynamoDBMapperModelFactory; + +import static org.junit.Assert.assertEquals; + +import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; +import java.util.TreeSet; +import java.util.UUID; + +import org.junit.Assert; +import org.junit.Test; + +import com.amazonaws.auth.AWSCredentialsProvider; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.mapper.dynamodb.pojos.SubClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.TestClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.UnannotatedSubClass; +import com.amazonaws.services.s3.model.Region; + +public class StandardModelFactoriesV2UnconvertTest { + + protected static final DynamoDBMapperConfig CONFIG = new DynamoDBMapperConfig.Builder() + .withTypeConverterFactory(DynamoDBMapperConfig.DEFAULT.getTypeConverterFactory()) + .withConversionSchema(ConversionSchemas.V2) + .build(); + + private static final DynamoDBMapperModelFactory factory = StandardModelFactories.of(new S3Link.Factory(new S3ClientCache((AWSCredentialsProvider)null))); + private static final DynamoDBMapperModelFactory.TableFactory models = factory.getTableFactory(CONFIG); + + protected Object unconvert(Class clazz, Method getter, Method setter, AttributeValue value) { + final StandardAnnotationMaps.FieldMap map = StandardAnnotationMaps.of(getter, null); + return models.getTable(clazz).field(map.attributeName()).unconvert(value); + } + + @Test + public void testBoolean() { + assertEquals(false, unconvert("getBoolean", "setBoolean", + AttributeValue.builder().n("0").build())); + + assertEquals(true, unconvert("getBoolean", "setBoolean", + AttributeValue.builder().n("1").build())); + + assertEquals(false, unconvert("getBoolean", "setBoolean", + AttributeValue.builder().bool(false).build())); + + assertEquals(true, unconvert("getBoolean", "setBoolean", + AttributeValue.builder().bool(true).build())); + + assertEquals(false, unconvert("getBoxedBoolean", "setBoxedBoolean", + AttributeValue.builder().n("0").build())); + + assertEquals(true, unconvert("getBoxedBoolean", "setBoxedBoolean", + AttributeValue.builder().n("1").build())); + + assertEquals(false, unconvert("getBoxedBoolean", "setBoxedBoolean", + AttributeValue.builder().bool(false).build())); + + assertEquals(true, unconvert("getBoxedBoolean", "setBoxedBoolean", + AttributeValue.builder().bool(true).build())); + } + + @Test + public void testString() { + assertEquals("test", unconvert("getString", "setString", + AttributeValue.builder().s("test").build())); + + Assert.assertNull(unconvert("getCustomString", "setCustomString", + AttributeValue.builder().s("ignoreme").build())); + } + + @Test + public void testUuid() { + UUID uuid = UUID.randomUUID(); + assertEquals(uuid, unconvert("getUuid", "setUuid", + AttributeValue.builder().s(uuid.toString()).build())); + } + + @Test + public void testDate() { + assertEquals(new Date(0), unconvert("getDate", "setDate", + AttributeValue.builder().s("1970-01-01T00:00:00.000Z").build())); + + Calendar c = GregorianCalendar.getInstance(); + c.setTimeInMillis(0); + + assertEquals(c, unconvert("getCalendar", "setCalendar", + AttributeValue.builder().s("1970-01-01T00:00:00.000Z").build())); + } + + @Test + public void testNumbers() { + assertEquals((byte) 1, unconvert("getByte", "setByte", + AttributeValue.builder().n("1").build())); + assertEquals((byte) 1, unconvert("getBoxedByte", "setBoxedByte", + AttributeValue.builder().n("1").build())); + + assertEquals((short) 1, unconvert("getShort", "setShort", + AttributeValue.builder().n("1").build())); + assertEquals((short) 1, unconvert("getBoxedShort", "setBoxedShort", + AttributeValue.builder().n("1").build())); + + assertEquals(1, unconvert("getInt", "setInt", + AttributeValue.builder().n("1").build())); + assertEquals(1, unconvert("getBoxedInt", "setBoxedInt", + AttributeValue.builder().n("1").build())); + + assertEquals(1l, unconvert("getLong", "setLong", + AttributeValue.builder().n("1").build())); + assertEquals(1l, unconvert("getBoxedLong", "setBoxedLong", + AttributeValue.builder().n("1").build())); + + assertEquals(BigInteger.ONE, unconvert("getBigInt", "setBigInt", + AttributeValue.builder().n("1").build())); + + assertEquals(1.5f, unconvert("getFloat", "setFloat", + AttributeValue.builder().n("1.5").build())); + assertEquals(1.5f, unconvert("getBoxedFloat", "setBoxedFloat", + AttributeValue.builder().n("1.5").build())); + + assertEquals(1.5d, unconvert("getDouble", "setDouble", + AttributeValue.builder().n("1.5").build())); + assertEquals(1.5d, unconvert("getBoxedDouble", "setBoxedDouble", + AttributeValue.builder().n("1.5").build())); + + assertEquals(BigDecimal.ONE, unconvert("getBigDecimal", "setBigDecimal", + AttributeValue.builder().n("1").build())); + } + + @Test + public void testBinary() { + ByteBuffer test = ByteBuffer.wrap("test".getBytes()); + Assert.assertTrue(Arrays.equals("test".getBytes(), (byte[]) unconvert( + "getByteArray", "setByteArray", + AttributeValue.builder().b(SdkBytes.fromByteBuffer(test.slice())).build()))); + + assertEquals(test.slice(), unconvert("getByteBuffer", "setByteBuffer", + AttributeValue.builder().b(SdkBytes.fromByteBuffer(test.slice())).build())); + } + + @Test + public void testBooleanSet() { + assertEquals(new HashSet() {{ add(true); }}, + unconvert("getBooleanSet", "setBooleanSet", + AttributeValue.builder().ns("1").build())); + + assertEquals(new HashSet() {{ add(false); }}, + unconvert("getBooleanSet", "setBooleanSet", + AttributeValue.builder().ns("0").build())); + + assertEquals(new HashSet() {{ add(true); add(false); }}, + unconvert("getBooleanSet", "setBooleanSet", + AttributeValue.builder().ns("0", "1").build())); + + assertEquals(new HashSet() {{ add(true); }}, + unconvert("getBooleanSet", "setBooleanSet", + AttributeValue.builder().l( + AttributeValue.builder().bool(true).build()).build())); + + assertEquals(new HashSet() {{ add(false); }}, + unconvert("getBooleanSet", "setBooleanSet", + AttributeValue.builder().l( + AttributeValue.builder().bool(false).build()).build())); + + assertEquals(new HashSet() {{ add(false); add(true); }}, + unconvert("getBooleanSet", "setBooleanSet", + AttributeValue.builder().l( + AttributeValue.builder().bool(false).build(), + AttributeValue.builder().bool(true).build()).build())); + + assertEquals(new HashSet() {{ add(null); }}, + unconvert("getBooleanSet", "setBooleanSet", + AttributeValue.builder().l( + AttributeValue.builder().nul(true).build()).build())); + } + + @Test + public void testStringSet() { + Assert.assertNull(unconvert("getStringSet", "setStringSet", + AttributeValue.builder().nul(true).build())); + + assertEquals(new HashSet() {{ add("a"); add("b"); }}, + unconvert("getStringSet", "setStringSet", + AttributeValue.builder().ss("a", "b").build())); + } + + @Test + public void testUuidSet() { + Assert.assertNull(unconvert("getUuidSet", "setUuidSet", + AttributeValue.builder().nul(true).build())); + + final UUID one = UUID.randomUUID(); + final UUID two = UUID.randomUUID(); + + assertEquals(new HashSet() {{ add(one); add(two); }}, + unconvert("getUuidSet", "setUuidSet", + AttributeValue.builder().ss( + one.toString(), + two.toString()).build())); + } + + @Test + public void testDateSet() { + assertEquals(Collections.singleton(new Date(0)), + unconvert("getDateSet", "setDateSet", AttributeValue.builder() + .ss("1970-01-01T00:00:00.000Z").build())); + + Calendar c = GregorianCalendar.getInstance(); + c.setTimeInMillis(0); + + assertEquals(Collections.singleton(c), + unconvert("getCalendarSet", "setCalendarSet", + AttributeValue.builder() + .ss("1970-01-01T00:00:00.000Z").build())); + } + + @Test + public void testNumberSet() { + Assert.assertNull(unconvert("getByteSet", "setByteSet", + AttributeValue.builder().nul(true).build())); + Assert.assertNull(unconvert("getShortSet", "setShortSet", + AttributeValue.builder().nul(true).build())); + Assert.assertNull(unconvert("getIntSet", "setIntSet", + AttributeValue.builder().nul(true).build())); + Assert.assertNull(unconvert("getLongSet", "setLongSet", + AttributeValue.builder().nul(true).build())); + Assert.assertNull(unconvert("getBigIntegerSet", "setBigIntegerSet", + AttributeValue.builder().nul(true).build())); + Assert.assertNull(unconvert("getFloatSet", "setFloatSet", + AttributeValue.builder().nul(true).build())); + Assert.assertNull(unconvert("getDoubleSet", "setDoubleSet", + AttributeValue.builder().nul(true).build())); + Assert.assertNull(unconvert("getBigDecimalSet", "setBigDecimalSet", + AttributeValue.builder().nul(true).build())); + + + assertEquals(new HashSet() {{ add((byte) 1); }}, + unconvert("getByteSet", "setByteSet", + AttributeValue.builder().ns("1").build())); + + assertEquals(new HashSet() {{ add((short) 1); }}, + unconvert("getShortSet", "setShortSet", + AttributeValue.builder().ns("1").build())); + + assertEquals(new HashSet() {{ add(1); }}, + unconvert("getIntSet", "setIntSet", + AttributeValue.builder().ns("1").build())); + + assertEquals(new HashSet() {{ add(1l); }}, + unconvert("getLongSet", "setLongSet", + AttributeValue.builder().ns("1").build())); + + assertEquals(new HashSet() {{ add(BigInteger.ONE); }}, + unconvert("getBigIntegerSet", "setBigIntegerSet", + AttributeValue.builder().ns("1").build())); + + assertEquals(new HashSet() {{ add(1.5f); }}, + unconvert("getFloatSet", "setFloatSet", + AttributeValue.builder().ns("1.5").build())); + + assertEquals(new HashSet() {{ add(1.5d); }}, + unconvert("getDoubleSet", "setDoubleSet", + AttributeValue.builder().ns("1.5").build())); + + assertEquals(new HashSet() {{ add(BigDecimal.ONE); }}, + unconvert("getBigDecimalSet", "setBigDecimalSet", + AttributeValue.builder().ns("1").build())); + } + + @Test + public void testBinarySet() { + Assert.assertNull(unconvert("getByteArraySet", "setByteArraySet", + AttributeValue.builder().nul(true).build())); + Assert.assertNull(unconvert("getByteBufferSet", "setByteBufferSet", + AttributeValue.builder().nul(true).build())); + + ByteBuffer test = ByteBuffer.wrap("test".getBytes()); + + Set result = (Set) unconvert( + "getByteArraySet", "setByteArraySet", + AttributeValue.builder().bs(SdkBytes.fromByteBuffer(test.slice())).build()); + + assertEquals(1, result.size()); + Assert.assertTrue(Arrays.equals( + "test".getBytes(), + result.iterator().next())); + + Assert.assertEquals(Collections.singleton(test.slice()), + unconvert("getByteBufferSet", "setByteBufferSet", + AttributeValue.builder().bs(SdkBytes.fromByteBuffer(test.slice())).build())); + } + + @Test + public void testObjectSet() { + Object result = unconvert("getObjectSet", "setObjectSet", + AttributeValue.builder().l(AttributeValue.builder().m( + new HashMap() {{ + put("name", AttributeValue.builder().s("name").build()); + put("value", AttributeValue.builder().n("123").build()); + put("null", AttributeValue.builder().nul(true).build()); + }} + ).build()).build()); + + assertEquals(Collections.singleton(new SubClass()), result); + + result = unconvert("getObjectSet", "setObjectSet", + AttributeValue.builder().l( + AttributeValue.builder().nul(true).build()).build()); + + assertEquals(Collections.singleton(null), result); + } + + @Test + public void testList() { + Assert.assertNull(unconvert("getList", "setList", + AttributeValue.builder().nul(true).build())); + + assertEquals(Arrays.asList("a", "b", "c"), + unconvert("getList", "setList", AttributeValue.builder().l( + AttributeValue.builder().s("a").build(), + AttributeValue.builder().s("b").build(), + AttributeValue.builder().s("c").build()).build())); + + assertEquals(Arrays.asList("a", null), + unconvert("getList", "setList", AttributeValue.builder().l( + AttributeValue.builder().s("a").build(), + AttributeValue.builder().nul(true).build()).build())); + } + + @Test + public void testObjectList() { + Assert.assertNull(unconvert("getObjectList", "setObjectList", + AttributeValue.builder().nul(true).build())); + + assertEquals(Arrays.asList(new SubClass(), null), + unconvert("getObjectList", "setObjectList", + AttributeValue.builder().l( + AttributeValue.builder().m(new HashMap() {{ + put("name", AttributeValue.builder().s("name").build()); + put("value", AttributeValue.builder().n("123").build()); + put("null", AttributeValue.builder().nul(true).build()); + }}).build(), + AttributeValue.builder().nul(true).build()).build())); + } + + @Test + public void testSetList() { + Assert.assertNull(unconvert("getSetList", "setSetList", + AttributeValue.builder().nul(true).build())); + + assertEquals(Arrays.asList(new Set[] { null }), + unconvert("getSetList", "setSetList", AttributeValue.builder().l( + AttributeValue.builder().nul(true).build()).build())); + + assertEquals(Arrays.asList(Collections.singleton("a")), + unconvert("getSetList", "setSetList", AttributeValue.builder().l( + AttributeValue.builder().ss("a").build()).build())); + } + + @Test + public void testMap() { + Assert.assertNull(unconvert("getMap", "setMap", + AttributeValue.builder().nul(true).build())); + + assertEquals(new HashMap() {{ + put("a", "b"); + put("c", "d"); + }}, + unconvert("getMap", "setMap", AttributeValue.builder().m( + new HashMap() {{ + put("a", AttributeValue.builder().s("b").build()); + put("c", AttributeValue.builder().s("d").build()); + }}).build())); + + assertEquals(new HashMap() {{ + put("a", null); + }}, + unconvert("getMap", "setMap", AttributeValue.builder().m( + new HashMap() {{ + put("a", AttributeValue.builder().nul(true).build()); + }}).build())); + } + + @Test + public void testSetMap() { + Assert.assertNull(unconvert("getSetMap", "setSetMap", + AttributeValue.builder().nul(true).build())); + + assertEquals(new HashMap>() {{ + put("a", null); + put("b", new TreeSet(Arrays.asList("a", "b"))); + }}, + unconvert("getSetMap", "setSetMap", AttributeValue.builder().m( + new HashMap() {{ + put("a", AttributeValue.builder().nul(true).build()); + put("b", AttributeValue.builder().ss("a", "b").build()); + }}).build())); + } + + @Test + public void testObject() { + Assert.assertNull(unconvert("getObject", "setObject", + AttributeValue.builder().nul(true).build())); + + assertEquals(new SubClass(), unconvert("getObject", "setObject", + AttributeValue.builder().m(new HashMap() {{ + put("name", AttributeValue.builder().s("name").build()); + put("value", AttributeValue.builder().n("123").build()); + }}).build())); + + assertEquals(new SubClass(), unconvert("getObject", "setObject", + AttributeValue.builder().m(new HashMap() {{ + put("name", AttributeValue.builder().s("name").build()); + put("value", AttributeValue.builder().n("123").build()); + put("null", AttributeValue.builder().nul(true).build()); + }}).build())); + } + + @Test + public void testUnannotatedObject() throws Exception { + Method getter = UnannotatedSubClass.class.getMethod("getChild"); + Method setter = UnannotatedSubClass.class + .getMethod("setChild", UnannotatedSubClass.class); + + try { + unconvert(UnannotatedSubClass.class, getter, setter, AttributeValue.builder().s("").build()); + Assert.fail("Expected DynamoDBMappingException"); + } catch (DynamoDBMappingException e) { + } + } + + @Test + public void testS3Link() { + S3Link link = (S3Link) unconvert("getS3Link", "setS3Link", + AttributeValue.builder().s("{\"s3\":{" + + "\"bucket\":\"bucket\"," + + "\"key\":\"key\"," + + "\"region\":null}}").build()); + + assertEquals("bucket", link.getBucketName()); + assertEquals("key", link.getKey()); + assertEquals(Region.US_Standard, link.getS3Region()); + } + + public Object unconvert(String getter, String setter, AttributeValue value) { + try { + + Method gm = TestClass.class.getMethod(getter); + Method sm = TestClass.class.getMethod(setter, gm.getReturnType()); + return unconvert(TestClass.class, gm, sm, value); + + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException("BOOM", e); + } + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/StringListMapTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/StringListMapTest.java new file mode 100644 index 000000000000..91ea71efe5b6 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/StringListMapTest.java @@ -0,0 +1,79 @@ +/* + * 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.mapper.dynamodb; + +import static org.junit.Assert.assertEquals; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper.StringListMap; + +import java.util.List; +import java.util.Map.Entry; + +import org.junit.Test; + +/** + * Unit tests for {@link StringListMap}. + */ +public class StringListMapTest { + + private StringListMap map() { + final StringListMap map = new StringListMap(); + for (int i = 1, its = 1; i <= its; i++) { map.add("A", Integer.valueOf(i)); } + for (int i = 1, its = 25; i <= its; i++) { map.add("B", Integer.valueOf(i)); } + for (int i = 1, its = 10; i <= its; i++) { map.add("C", Integer.valueOf(i)); } + for (int i = 1, its = 5; i <= its; i++) { map.add("D", Integer.valueOf(i)); } + for (int i = 1, its = 10; i <= its; i++) { map.add("E", Integer.valueOf(i)); } + for (int i = 1, its = 25; i <= its; i++) { map.add("F", Integer.valueOf(i)); } + for (int i = 1, its = 1; i <= its; i++) { map.add("G", Integer.valueOf(i)); } + assertEquals(7, map.size()); + return map; + } + + @Test + public void testSubMaps20PerMap() { + assertSizes(map().subMaps(20, true), new int[][]{ {1,19}, {6,10,4}, {1,10,9}, {16,1} }); + } + + @Test + public void testSubMaps11PerMap() { + assertSizes(map().subMaps(11, true), new int[][]{ {1,10}, {11}, {4,7}, {3,5,3}, {7,4}, {11}, {10,1} }); + } + + @Test + public void testSubMapsInto4() { + assertSizes(map().subMaps(4, false), new int[][]{ {1,6,2,2,2,6,1}, {7,2,1,3,6}, {6,3,1,3,6}, {6,3,1,2,7} }); + } + + private void assertSizes(List> subMaps, int[][] sizes) { + try { + assertEquals(sizes.length, subMaps.size()); + + for (int mapIndex = 0, keyIndex = 0; mapIndex < sizes.length; mapIndex++, keyIndex = 0) { + final StringListMap subMap = subMaps.get(mapIndex); + assertEquals("subMaps[" + mapIndex + "]", sizes[mapIndex].length, subMap.size()); + + for (final List values : subMap.values()) { + assertEquals("subMaps[" + mapIndex + "][" + keyIndex + "]", sizes[mapIndex][keyIndex], values.size()); + keyIndex++; + } + } + } catch (final Error e) { + System.out.println("subMaps=" + subMaps); + throw e; + } + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/TestObjectCreator.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/TestObjectCreator.java new file mode 100644 index 000000000000..e5a0924419d6 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/TestObjectCreator.java @@ -0,0 +1,1245 @@ +/* + * 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.mapper.dynamodb; + +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; + +import static software.amazon.awssdk.mapper.dynamodb.pojos.CustomBooleanClass.CustomBoolean; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTransactionLoadExpression; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTransactionWriteExpression; +import software.amazon.awssdk.mapper.dynamodb.TransactionLoadRequest; +import software.amazon.awssdk.mapper.dynamodb.TransactionWriteRequest; +import software.amazon.awssdk.mapper.dynamodb.TransactionWriteRequest.TransactionWriteOperation; +import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest; +import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; +import software.amazon.awssdk.services.dynamodb.model.DescribeTableResponse; +import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement; +import software.amazon.awssdk.services.dynamodb.model.KeyType; +import software.amazon.awssdk.services.dynamodb.model.ProvisionedThroughput; +import software.amazon.awssdk.services.dynamodb.model.ResourceNotFoundException; +import software.amazon.awssdk.services.dynamodb.model.ReturnValuesOnConditionCheckFailure; +import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType; +import software.amazon.awssdk.services.dynamodb.model.TableStatus; +import software.amazon.awssdk.mapper.dynamodb.pojos.AllSupportedAnnotationsClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.AllSupportedDataTypesClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.Currency; +import software.amazon.awssdk.mapper.dynamodb.pojos.CustomBooleanClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.DateRange; +import software.amazon.awssdk.mapper.dynamodb.pojos.HashKeyRangeKeyBothAutoGenerated; +import software.amazon.awssdk.mapper.dynamodb.pojos.HashKeyRangeKeyClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.HashRangeAndOtherAttributeAutoGen; +import software.amazon.awssdk.mapper.dynamodb.pojos.MultiVersionRangeKeyClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.PhoneNumber; +import software.amazon.awssdk.mapper.dynamodb.pojos.RangeKeyClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.SchemaViolatingTestItem; +import software.amazon.awssdk.mapper.dynamodb.pojos.SlimAttributeNamesClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.StringAttributeClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.TestItem; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +public class TestObjectCreator { + + public static final ProvisionedThroughput DEFAULT_PROVISIONED_THROUGHPUT = + ProvisionedThroughput.builder().readCapacityUnits(10L).writeCapacityUnits(10L).build(); + + private static final String AWS_JAVA_SDK_UTIL_TABLE_NAME = "aws-java-sdk-util"; + private static final String AWS_JAVA_SDK_DYNAMODB_MAPPER_SAVE_CONFIG_TEST_TABLE_NAME = "aws-java-sdk-dynamodb-mapper-save-config-test"; + private static final String AWS_JAVA_SDK_RANGE_TEST_TABLE_NAME = "aws-java-sdk-range-test"; + private static final String AWS_JAVA_SDK_STRING_RANGE_TABLE_NAME = "aws-java-sdk-string-range"; + private static final String AWS_JAVA_SDK_DOC_ATTRIBUTE_TABLE = "aws-java-sdk-doc-attribute-table"; + private static final String NON_MATCHING_STRING = "NON_MATCHING_STRING"; + + // Some random big integer + private static int start = Integer.parseInt(String.valueOf(System.currentTimeMillis() / 10000)); + private static long startKey = System.currentTimeMillis(); + + public static class TransactionLoadTestData { + private TransactionLoadRequest transactionLoadRequest; + private List expectedObjects; + + public TransactionLoadTestData(TransactionLoadRequest transactionLoadRequest, List expectedObjects) { + this.transactionLoadRequest = transactionLoadRequest; + this.expectedObjects = expectedObjects; + } + + public TransactionLoadRequest getTransactionLoadRequest() { + return transactionLoadRequest; + } + + public List getExpectedObjects() { + return expectedObjects; + } + } + + /** + * Used to specify number of objects in a {@link TransactionLoadRequest} which have corresponding existing items + */ + public enum Existence { + ALL, SOME, NONE + } + + /** + * Interface that defines object supplier used when creating objects of same type + */ + public interface ObjectSupplier { + Object get(); + } + + /** + * Used to specify the type of projection expressions to use while creating a {@link TransactionLoadRequest} + */ + public enum ProjectionExpressionMode { + NONE, // No projection expressions for any of the objects + + ONE, // One object with projection expression (but without expressionAttributeNames), no projection expressions for any of the other objects + + MULTIPLE, // Multiple objects with projection expressions (but without expressionAttributeNames) + + ONE_ATTRIBUTE_NAMES, // Multiple objects with projection expressions but only one with expressionAttributeNames + + MULTIPLE_ATTRIBUTE_NAMES, // Multiple objects with projection expressions and expressionAttributeNames + + ONE_RESERVED_WORD, // One object with projection expression (but without expressionAttributeNames) containing a reserved word, no projection expressions for any of the other objects + + MIXED_NON_EXISTING_ATTRIBUTES // One object with expression projecting some existing, some non-existing attributes and one object with expression projecting all non-existing attributes. + // None of these two objects have expressionAttributeNames and no projection expressions for any of the other objects + } + + public static class TransactionLoadTestDataRequest { + private int numObjects = 5; + private Existence existence = Existence.SOME; + private ProjectionExpressionMode projectionExpressionMode = ProjectionExpressionMode.NONE; + private boolean useSameObjectType = false; + private ObjectSupplier objectSupplier; + + public TransactionLoadTestDataRequest withNumObjects(int numObjects) { + if (numObjects < 0) { + throw new IllegalArgumentException("numObjects must be greater than zero"); + } + this.numObjects = numObjects; + return this; + } + + public TransactionLoadTestDataRequest withExistence(Existence existence) { + if (existence == null) { + throw new IllegalArgumentException("existence must be non-null"); + } + this.existence = existence; + return this; + } + + public TransactionLoadTestDataRequest withProjectionExpressionMode(ProjectionExpressionMode projectionExpressionMode) { + if (projectionExpressionMode == null) { + throw new IllegalArgumentException("projectionExpressionMode must be non-null"); + } + this.projectionExpressionMode = projectionExpressionMode; + return this; + } + + public TransactionLoadTestDataRequest withUseSameObjectType(boolean useSameObjectType) { + this.useSameObjectType = useSameObjectType; + return this; + } + + public TransactionLoadTestDataRequest withObjectSupplier(ObjectSupplier objectSupplier) { + if (objectSupplier == null) { + throw new IllegalArgumentException("objectSupplier must be non-null"); + } + if (!useSameObjectType) { + throw new IllegalArgumentException("useSameObjectType must be set to true before setting objectSupplier"); + } + this.objectSupplier = objectSupplier; + return this; + } + } + + public static TransactionLoadTestData createMultipleObjectTestDataForTransactionLoad(DynamoDBMapper mapper, TransactionLoadTestDataRequest request) { + List expectedObjects = new ArrayList(); + TransactionLoadRequest transactionLoadRequest = new TransactionLoadRequest(); + for (int i = 0; i < request.numObjects; i++) { + Object obj = getTestObject(request.useSameObjectType, request.objectSupplier, i); + TransactionLoadExpressionData expressionData = generateTransactionLoadExpressionData(obj, request.projectionExpressionMode, i); + transactionLoadRequest.addLoad(obj, expressionData.transactionLoadExpression); + + switch (request.existence) { + case ALL: + mapper.save(obj); + expectedObjects.add(expressionData.expectedObject); + break; + case SOME: + if (i % 2 == 0) { + mapper.save(obj); + expectedObjects.add(expressionData.expectedObject); + } else { + expectedObjects.add(null); + } + break; + case NONE: + expectedObjects.add(null); + break; + default: + throw new IllegalArgumentException("Unhandled existence: " + request.existence); + } + } + return new TransactionLoadTestData(transactionLoadRequest, expectedObjects); + } + + private static class TransactionLoadExpressionData { + DynamoDBTransactionLoadExpression transactionLoadExpression; + Object expectedObject; + + public TransactionLoadExpressionData(DynamoDBTransactionLoadExpression transactionLoadExpression, Object expectedObject) { + this.transactionLoadExpression = transactionLoadExpression; + this.expectedObject = expectedObject; + } + } + + private enum AttributeProjection { + SOME, ALL, SOME_NON_EXISTING, ALL_NON_EXISTING + } + + private static final Map> stringAttributeClassAttributeProjections; + static { + stringAttributeClassAttributeProjections = new HashMap>(); + stringAttributeClassAttributeProjections.put(AttributeProjection.SOME, Arrays.asList("stringAttribute")); + stringAttributeClassAttributeProjections.put(AttributeProjection.ALL, Arrays.asList("key", "stringAttribute", "originalName")); + stringAttributeClassAttributeProjections.put(AttributeProjection.SOME_NON_EXISTING, Arrays.asList("nonExistingAttribute1", "originalName", "nonExistingAttribute2")); + stringAttributeClassAttributeProjections.put(AttributeProjection.ALL_NON_EXISTING, Arrays.asList("nonExistingAttribute1", "nonExistingAttribute2", "nonExistingAttribute3")); + } + private static final Map> testItemClassAttributeProjections; + static { + testItemClassAttributeProjections = new HashMap>(); + testItemClassAttributeProjections.put(AttributeProjection.SOME, Arrays.asList("rangeKey", "stringAttribute", "stringSetAttribute")); + testItemClassAttributeProjections.put(AttributeProjection.ALL, Arrays.asList("hashKey", "rangeKey", "stringAttribute", "nonKeyAttribute", "stringSetAttribute")); + testItemClassAttributeProjections.put(AttributeProjection.SOME_NON_EXISTING, Arrays.asList("nonExistingAttribute1", "nonKeyAttribute", "nonExistingAttribute2")); + testItemClassAttributeProjections.put(AttributeProjection.ALL_NON_EXISTING, Arrays.asList("nonExistingAttribute1", "nonExistingAttribute2", "nonExistingAttribute3")); + } + private static final Map> rangeKeyClassAttributeProjections; + static { + rangeKeyClassAttributeProjections = new HashMap>(); + rangeKeyClassAttributeProjections.put(AttributeProjection.SOME, Arrays.asList("rangeKey", "stringAttribute", "integerSetAttribute")); + rangeKeyClassAttributeProjections.put(AttributeProjection.ALL, Arrays.asList("key", "rangeKey", "version", "integerSetAttribute", "stringAttribute", "stringSetAttribute", "bigDecimalAttribute")); + rangeKeyClassAttributeProjections.put(AttributeProjection.SOME_NON_EXISTING, Arrays.asList("nonExistingAttribute1", "bigDecimalAttribute", "nonExistingAttribute2")); + rangeKeyClassAttributeProjections.put(AttributeProjection.ALL_NON_EXISTING, Arrays.asList("nonExistingAttribute1", "nonExistingAttribute2", "nonExistingAttribute3")); + } + + private static TransactionLoadExpressionData generateTransactionLoadExpressionData(Object object, ProjectionExpressionMode expressionMode, int objectNumber) { + DynamoDBTransactionLoadExpression transactionLoadExpression = null; + Object expectedObject = object; + switch (expressionMode) { + case NONE: + break; + case ONE: + if (objectNumber == 0) { + transactionLoadExpression = getTransactionLoadExpression(stringAttributeClassAttributeProjections.get(AttributeProjection.SOME)); + expectedObject = generateExpectedStringAttributeClassObject((StringAttributeClass) object, AttributeProjection.SOME, false /* excludeKey */); + } + break; + case MULTIPLE: + if (objectNumber == 0) { + return generateTransactionLoadExpressionData(object, AttributeProjection.ALL, true /* excludeKey */); + } else { + return generateTransactionLoadExpressionData(object, AttributeProjection.SOME, false /* excludeKey */); + } + case ONE_ATTRIBUTE_NAMES: + if (objectNumber == 0) { + transactionLoadExpression = getTransactionLoadExpressionWithExpressionAttributeNames(stringAttributeClassAttributeProjections.get(AttributeProjection.SOME)); + expectedObject = generateExpectedStringAttributeClassObject((StringAttributeClass) object, AttributeProjection.SOME, false /* excludeKey */); + } else { + return generateTransactionLoadExpressionData(object, AttributeProjection.SOME, false /* excludeKey */); + } + break; + case MULTIPLE_ATTRIBUTE_NAMES: + if (objectNumber < 3) { + return generateTransactionLoadExpressionData(object, AttributeProjection.SOME, false /* excludeKey */); + } else { + return generateTransactionLoadExpressionDataWithExpressionAttributeNames(object, AttributeProjection.ALL); + } + case ONE_RESERVED_WORD: + if (objectNumber == 0) { + transactionLoadExpression = getTransactionLoadExpressionContainingReservedWord(); + expectedObject = null; + } + break; + case MIXED_NON_EXISTING_ATTRIBUTES: + if (objectNumber == 0) { + transactionLoadExpression = getTransactionLoadExpression(stringAttributeClassAttributeProjections.get(AttributeProjection.ALL_NON_EXISTING)); + expectedObject = generateExpectedStringAttributeClassObject((StringAttributeClass) object, AttributeProjection.ALL_NON_EXISTING, false /* excludeKey */); + } else if (objectNumber == 1) { + transactionLoadExpression = getTransactionLoadExpression(testItemClassAttributeProjections.get(AttributeProjection.SOME_NON_EXISTING)); + expectedObject = generateExpectedTestItemObject((TestItem) object, AttributeProjection.SOME_NON_EXISTING); + } + break; + default: + throw new IllegalArgumentException("Unsupported projectionExpressionMode: " + expressionMode); + } + return new TransactionLoadExpressionData(transactionLoadExpression, expectedObject); + } + + /** + * Since this method doesn't use expressionAttributeNames for generating load expression data, if an attribute name is "key" then we may want to remove it + * from projected attributes as it is a reserved keyword. If excludeKey is true then attribute with name "key" is removed from projected attributes and not + * included in expected object. + */ + private static TransactionLoadExpressionData generateTransactionLoadExpressionData(Object object, AttributeProjection attributeProjection, boolean excludeKey) { + List attributeNamesToProject; + Object expectedObject; + if (StringAttributeClass.class.equals(object.getClass())) { + attributeNamesToProject = stringAttributeClassAttributeProjections.get(attributeProjection); + expectedObject = generateExpectedStringAttributeClassObject((StringAttributeClass) object, attributeProjection, excludeKey /* excludeKey */); + } else if (TestItem.class.equals(object.getClass())) { + attributeNamesToProject = testItemClassAttributeProjections.get(attributeProjection); + expectedObject = generateExpectedTestItemObject((TestItem) object, attributeProjection); + } else if (RangeKeyClass.class.equals(object.getClass())) { + attributeNamesToProject = rangeKeyClassAttributeProjections.get(attributeProjection); + expectedObject = generateExpectedRangeKeyClassObject((RangeKeyClass) object, attributeProjection, excludeKey); + } else { + throw new IllegalArgumentException("Unsupported object type: " + object.getClass()); + } + if (excludeKey) { + attributeNamesToProject = excludeKeyFromAttributeNames(attributeNamesToProject); + } + DynamoDBTransactionLoadExpression transactionLoadExpression = getTransactionLoadExpression(attributeNamesToProject); + return new TransactionLoadExpressionData(transactionLoadExpression, expectedObject); + } + + private static List excludeKeyFromAttributeNames(List attributeNames) { + List result = new ArrayList(); + for (String attributeName: attributeNames) { + if (!"key".equalsIgnoreCase(attributeName)) { + result.add(attributeName); + } + } + return result; + } + + private static TransactionLoadExpressionData generateTransactionLoadExpressionDataWithExpressionAttributeNames(Object object, AttributeProjection attributeProjection) { + Object expectedObject; + List attributeNamesToProject; + if (StringAttributeClass.class.equals(object.getClass())) { + attributeNamesToProject = stringAttributeClassAttributeProjections.get(attributeProjection); + expectedObject = generateExpectedStringAttributeClassObject((StringAttributeClass) object, attributeProjection, false /* excludeKey */); + } else if (TestItem.class.equals(object.getClass())) { + attributeNamesToProject = testItemClassAttributeProjections.get(attributeProjection); + expectedObject = generateExpectedTestItemObject((TestItem) object, attributeProjection); + } else if (RangeKeyClass.class.equals(object.getClass())) { + attributeNamesToProject = rangeKeyClassAttributeProjections.get(attributeProjection); + expectedObject = generateExpectedRangeKeyClassObject((RangeKeyClass) object, attributeProjection, false /* excludeKey */); + } else { + throw new IllegalArgumentException("Unsupported object type: " + object.getClass()); + } + DynamoDBTransactionLoadExpression transactionLoadExpression = getTransactionLoadExpressionWithExpressionAttributeNames(attributeNamesToProject); + return new TransactionLoadExpressionData(transactionLoadExpression, expectedObject); + } + + private static StringAttributeClass generateExpectedStringAttributeClassObject(StringAttributeClass sourceObject, AttributeProjection attributeProjection, boolean excludeKey) { + StringAttributeClass expectedObject = new StringAttributeClass(); + switch (attributeProjection) { + case SOME: + expectedObject.setStringAttribute(sourceObject.getStringAttribute()); + return expectedObject; + case ALL: + if (!excludeKey) { + expectedObject.setKey(sourceObject.getKey()); + } + expectedObject.setStringAttribute(sourceObject.getStringAttribute()); + expectedObject.setRenamedAttribute(sourceObject.getRenamedAttribute()); + return expectedObject; + case SOME_NON_EXISTING: + expectedObject.setRenamedAttribute(sourceObject.getRenamedAttribute()); + return expectedObject; + case ALL_NON_EXISTING: + return null; + default: + throw new IllegalArgumentException("Unsupported attributeProjection: " + attributeProjection); + } + } + + private static TestItem generateExpectedTestItemObject(TestItem sourceObject, AttributeProjection attributeProjection) { + TestItem expectedObject = new TestItem(); + switch (attributeProjection) { + case SOME: + expectedObject.setRangeKey(sourceObject.getRangeKey()); + expectedObject.setStringAttribute(sourceObject.getStringAttribute()); + expectedObject.setStringSetAttribute(sourceObject.getStringSetAttribute()); + return expectedObject; + case ALL: + expectedObject.setHashKey(sourceObject.getHashKey()); + expectedObject.setRangeKey(sourceObject.getRangeKey()); + expectedObject.setStringAttribute(sourceObject.getStringAttribute()); + expectedObject.setNonKeyAttribute(sourceObject.getNonKeyAttribute()); + expectedObject.setStringSetAttribute(sourceObject.getStringSetAttribute()); + return expectedObject; + case SOME_NON_EXISTING: + expectedObject.setNonKeyAttribute(sourceObject.getNonKeyAttribute()); + return expectedObject; + case ALL_NON_EXISTING: + return null; + default: + throw new IllegalArgumentException("Unsupported attributeProjection: " + attributeProjection); + } + } + + private static RangeKeyClass generateExpectedRangeKeyClassObject(RangeKeyClass sourceObject, AttributeProjection attributeProjection, boolean excludeKey) { + RangeKeyClass expectedObject = new RangeKeyClass(); + switch (attributeProjection) { + case SOME: + expectedObject.setRangeKey(sourceObject.getRangeKey()); + expectedObject.setStringAttribute(sourceObject.getStringAttribute()); + expectedObject.setIntegerAttribute(sourceObject.getIntegerAttribute()); + return expectedObject; + case ALL: + if (!excludeKey) { + expectedObject.setKey(sourceObject.getKey()); + } + expectedObject.setRangeKey(sourceObject.getRangeKey()); + expectedObject.setVersion(sourceObject.getVersion()); + expectedObject.setIntegerAttribute(sourceObject.getIntegerAttribute()); + expectedObject.setStringAttribute(sourceObject.getStringAttribute()); + expectedObject.setStringSetAttribute(sourceObject.getStringSetAttribute()); + expectedObject.setBigDecimalAttribute(sourceObject.getBigDecimalAttribute()); + return expectedObject; + case SOME_NON_EXISTING: + expectedObject.setBigDecimalAttribute(sourceObject.getBigDecimalAttribute()); + return expectedObject; + case ALL_NON_EXISTING: + return null; + default: + throw new IllegalArgumentException("Unsupported attributeProjection: " + attributeProjection); + } + } + + private static DynamoDBTransactionLoadExpression getTransactionLoadExpression(List attributeNames) { + if (attributeNames.isEmpty()) { + return null; + } + StringBuilder projectionExpressionBuilder = new StringBuilder(attributeNames.get(0)); + for (int i = 1; i < attributeNames.size(); i++) { + projectionExpressionBuilder.append(", " + attributeNames.get(i)); + } + return new DynamoDBTransactionLoadExpression().withProjectionExpression(projectionExpressionBuilder.toString()); + } + + private static DynamoDBTransactionLoadExpression getTransactionLoadExpressionWithExpressionAttributeNames(List attributeNames) { + if (attributeNames.isEmpty()) { + return null; + } + String firstAttributeName = attributeNames.get(0); + String tokenBase = "u7q3j"; + String currentTokenString = "#" + tokenBase + "0"; + StringBuilder projectionExpressionBuilder = new StringBuilder(currentTokenString); + Map expressionAttributeNames = new HashMap(); + expressionAttributeNames.put(currentTokenString, firstAttributeName); + for (int i = 1; i < attributeNames.size(); i++) { + currentTokenString = "#" + tokenBase + i; + projectionExpressionBuilder.append(", " + currentTokenString); + expressionAttributeNames.put(currentTokenString, attributeNames.get(i)); + } + return new DynamoDBTransactionLoadExpression().withProjectionExpression(projectionExpressionBuilder.toString()) + .withExpressionAttributeNames(expressionAttributeNames); + } + + private static DynamoDBTransactionLoadExpression getTransactionLoadExpressionContainingReservedWord() { + return new DynamoDBTransactionLoadExpression().withProjectionExpression("stringAttribute, SET"); + } + + public static Object getTestObject(boolean useSameObjectType, ObjectSupplier objectSupplier, int tableNumber) { + if (useSameObjectType) { + if (objectSupplier != null) { + return objectSupplier.get(); + } + return getStringAttributeClass(); + } + if (tableNumber % 3 == 0) { + return getStringAttributeClass(); + } + if (tableNumber % 3 == 1) { + return getTestItemObject(); + } + return getUniqueRangeKeyObject(); + } + + public static Object getTestObject(boolean isMultiTable, int tableNumber) { + if (!isMultiTable) { + return getStringAttributeClass(); + } + if (tableNumber % 3 == 0) { + return getStringAttributeClass(); + } + if (tableNumber % 3 == 1) { + return getTestItemObject(); + } + return getHashKeyRangeKeyObject(); + } + + public static List getMultiTableObjects(int numItems) { + List objs = new ArrayList(); + for (int i = 0; i < numItems / 2; i++) { + StringAttributeClass obj = getStringAttributeClass(); + objs.add(obj); + } + for (int i = 0; i < numItems / 2; i++) { + TestItem obj = getTestItemObject(); + objs.add(obj); + } + return objs; + } + + public static StringAttributeClass getStringAttributeClass() { + StringAttributeClass obj = new StringAttributeClass(); + obj.setKey(String.valueOf(startKey++)); + obj.setRenamedAttribute(String.valueOf(startKey++)); + obj.setStringAttribute(String.valueOf(startKey++)); + return obj; + } + + public static TestItem getTestItemObject() { + TestItem obj = new TestItem(); + obj.setHashKey(UUID.randomUUID().toString()); + obj.setRangeKey(System.currentTimeMillis()); + obj.setStringAttribute(String.valueOf(startKey++)); + obj.setStringSetAttribute(generateRandomStringSet(3)); + obj.setNonKeyAttribute(String.valueOf(startKey++)); + return obj; + } + + public static SchemaViolatingTestItem getSchemaViolatingTestItemObject() { + SchemaViolatingTestItem obj = new SchemaViolatingTestItem(); + obj.setHashKey(UUID.randomUUID().toString()); + obj.setRangeKey(System.currentTimeMillis()); + return obj; + } + + public static RangeKeyClass getUniqueRangeKeyObject() { + RangeKeyClass obj = new RangeKeyClass(); + obj.setKey(startKey++); + obj.setIntegerAttribute(toSet(start++, start++, start++, start++)); + obj.setBigDecimalAttribute(new BigDecimal(startKey++)); + obj.setRangeKey(start++); + obj.setStringAttribute("" + startKey++); + obj.setStringSetAttribute(generateRandomStringSet(3)); + return obj; + } + + public static MultiVersionRangeKeyClass getMultiVersionRangeKeyObject() { + MultiVersionRangeKeyClass obj = new MultiVersionRangeKeyClass(); + obj.setKey(startKey++); + obj.setIntegerAttribute(toSet(start++, start++, start++, start++)); + obj.setBigDecimalAttribute(new BigDecimal(startKey++)); + obj.setRangeKey(start++); + obj.setStringAttribute("" + startKey++); + obj.setStringSetAttribute(generateRandomStringSet(3)); + return obj; + } + + public static HashKeyRangeKeyBothAutoGenerated getStringHashAndRangeKeyObjectWithKeys() { + HashKeyRangeKeyBothAutoGenerated obj = new HashKeyRangeKeyBothAutoGenerated(); + String uuid = UUID.randomUUID().toString(); + obj.setKey("hash_" + uuid); + obj.setRangeKey("range_" + uuid); + obj.setOtherAttribute("otherAttribute_" + uuid); + return obj; + } + + public static CustomBooleanClass getCustomBooleanClassObject() { + CustomBooleanClass obj = new CustomBooleanClass(); + String uuid = UUID.randomUUID().toString(); + obj.setKey(uuid); + obj.setCustomBooleanAttribute(new CustomBoolean(true)); + return obj; + } + + public static HashKeyRangeKeyClass getHashKeyRangeKeyObject() { + HashKeyRangeKeyClass obj = new HashKeyRangeKeyClass(); + obj.setHashKey(startKey++); + obj.setRangeKey(start++); + obj.setStringAttribute("" + startKey++); + return obj; + } + + public static AllSupportedDataTypesClass getAllSupportedDataTypesObject() { + AllSupportedDataTypesClass obj = new AllSupportedDataTypesClass(); + String uuid = UUID.randomUUID().toString(); + obj.setKey("hash_" + uuid); + obj.setRangeKey("range_" + uuid); + obj.setBigDecimalAttribute(new BigDecimal(startKey++)); + obj.setBigDecimalSetAttribute(toSet(new BigDecimal(startKey++), new BigDecimal(startKey++), new BigDecimal(startKey++))); + obj.setBigIntegerAttribute(BigInteger.valueOf(startKey++)); + obj.setBigIntegerSetAttribute(toSet(BigInteger.valueOf(startKey++), BigInteger.valueOf(startKey++), BigInteger.valueOf(startKey++))); + obj.setBooleanAttribute(start++ % 2 == 0); + obj.setBooleanSetAttribute(toSet(true, false)); + obj.setByteAttribute((byte) start++); + obj.setByteSetAttribute(toSet((byte) start++, (byte) start++, (byte) start++)); + obj.setCalendarAttribute(getCalendarObject()); + obj.setCalendarSetAttribute(toSet(getCalendarObject(), getCalendarObject(), getCalendarObject())); + obj.setDateAttribute(new Date(startKey++)); + obj.setDateSetAttribute(toSet(new Date(startKey++), new Date(startKey++), new Date(startKey++))); + obj.setDoubleAttribute((double) startKey++); + obj.setDoubleSetAttribute(toSet((double) startKey++, (double) startKey++, (double) startKey++)); + obj.setFloatAttribute((float) start++); + obj.setFloatSetAttribute(toSet((float) start++, (float) start++, (float) start++)); + obj.setIntegerAttribute(start++); + obj.setIntegerSetAttribute(toSet(start++, start++, start++)); + obj.setLongAttribute(startKey++); + obj.setLongSetAttribute(toSet(startKey++, startKey++, startKey++)); + obj.setStringAttribute("stringAttribute_" + uuid); + obj.setStringSetAttribute(toSet("stringSetAttribute_1_" + uuid, "stringSetAttribute_2_" + uuid, "stringSetAttribute_3_" + uuid)); + return obj; + } + + public static AllSupportedAnnotationsClass getAllSupportedAnnotationsObject() { + AllSupportedAnnotationsClass obj = new AllSupportedAnnotationsClass(); + String uuid = UUID.randomUUID().toString(); + obj.setId("hash_" + uuid); + obj.setRangeKey("range_" + uuid); + obj.setIntegerAttribute(start++); + obj.setBooleanAttribute(true); + obj.setPhoneNumber(new PhoneNumber("xyz", "867", "5309")); + obj.setIgnoredAttribute("IgnoredString_" + uuid); + obj.setGsiHashKey("gsi_hash_" + uuid); + obj.setGsiRangeKey("gsi_range_" + uuid); + obj.setNamedAttribute("namedAttribute_" + uuid); + obj.setCurrencyAttribute(new Currency((double) startKey++, "USD")); + obj.setEpochDate(new Date(startKey++)); + obj.setDateRange(new DateRange(new Date(startKey++), -60000L, 60000L)); + return obj; + } + + private static Calendar getCalendarObject() { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(new Date(startKey++)); + return calendar; + } + + public static HashRangeAndOtherAttributeAutoGen getHashRangeAutoGenOtherAttributeObject() { + HashRangeAndOtherAttributeAutoGen obj = new HashRangeAndOtherAttributeAutoGen(); + String uuid = UUID.randomUUID().toString(); + obj.setKey("hash_" + uuid); + obj.setRangeKey("range_" + uuid); + obj.setDate(new Date()); + return obj; + } + + public static SlimAttributeNamesClass getSlimAttributeNamesClass() { + SlimAttributeNamesClass obj = new SlimAttributeNamesClass(); + String uuid = UUID.randomUUID().toString(); + obj.setHashKey("hash_" + uuid); + obj.setRangeKey(startKey++); + obj.setZ("Z_" + uuid); + obj.setA("A_" + uuid); + obj.setL("L_" + uuid); + obj.setC("C_" + uuid); + obj.setG("G_" + uuid); + obj.setM("M_" + uuid); + return obj; + } + + private static Set generateRandomStringSet(int size) { + Set result = new HashSet(); + for (int i = 0; i < size; i++) { + result.add(UUID.randomUUID().toString()); + } + return result; + } + + private static Set toSet(T... array) { + Set set = new HashSet(); + for (T t : array) { + set.add(t); + } + return set; + } + + public static void createStringHashKeyTable(DynamoDbClient dynamoDB) throws InterruptedException { + createStringHashKeyTable(dynamoDB, AWS_JAVA_SDK_UTIL_TABLE_NAME); + } + + public static void createStringHashKeyTable(DynamoDbClient dynamoDB, String tableName) throws InterruptedException { + createTestTable(dynamoDB, DEFAULT_PROVISIONED_THROUGHPUT, + tableName, + "key" /* hashKeyName */, + ScalarAttributeType.S, + null /* rangeKeyName */, + null /* rangeKeyAttributeType */); + } + + public static void createSdkMapperSaveConfigTable(DynamoDbClient dynamoDB) throws InterruptedException { + createSdkMapperSaveConfigTable(dynamoDB, AWS_JAVA_SDK_DYNAMODB_MAPPER_SAVE_CONFIG_TEST_TABLE_NAME); + } + + public static void createSdkMapperSaveConfigTable(DynamoDbClient dynamoDB, String tableName) throws InterruptedException { + createTestTable(dynamoDB, DEFAULT_PROVISIONED_THROUGHPUT, + tableName, + "hashKey", + ScalarAttributeType.S, + "rangeKey", + ScalarAttributeType.N); + } + + public static void createSdkRangeTestTable(DynamoDbClient dynamoDB) throws InterruptedException { + createSdkRangeTestTable(dynamoDB, AWS_JAVA_SDK_RANGE_TEST_TABLE_NAME); + } + + public static void createSdkRangeTestTable(DynamoDbClient dynamoDB, String tableName) throws InterruptedException { + createTestTable(dynamoDB, DEFAULT_PROVISIONED_THROUGHPUT, + tableName, + "key", + ScalarAttributeType.N, + "rangeKey", + ScalarAttributeType.N); + } + + public static void createSdkStringRangeTable(DynamoDbClient dynamoDB) throws InterruptedException { + createSdkStringRangeTable(dynamoDB, AWS_JAVA_SDK_STRING_RANGE_TABLE_NAME); + } + + public static void createSdkStringRangeTable(DynamoDbClient dynamoDB, String tableName) throws InterruptedException { + createTestTable(dynamoDB, DEFAULT_PROVISIONED_THROUGHPUT, + tableName, + "key", + ScalarAttributeType.S, + "rangeKey", + ScalarAttributeType.S); + } + + public static void createHashKeyRangeKeyTable(DynamoDbClient dynamoDB, String tableName) throws InterruptedException { + createTestTable(dynamoDB, DEFAULT_PROVISIONED_THROUGHPUT, + tableName, + "hashKey", + ScalarAttributeType.N, + "rangeKey", + ScalarAttributeType.N); + } + + public static void createSdkDocAttributeTable(DynamoDbClient dynamoDB) throws InterruptedException { + createSdkDocAttributeTable(dynamoDB, AWS_JAVA_SDK_DOC_ATTRIBUTE_TABLE); + } + + public static void createSdkDocAttributeTable(DynamoDbClient dynamoDB, String tableName) throws InterruptedException { + createTestTable(dynamoDB, DEFAULT_PROVISIONED_THROUGHPUT, + tableName, + "id" /* hashKeyName */, + ScalarAttributeType.S, + null /* rangeKeyName */, + null /* rangeKeyAttributeType */); + } + + public static void createAllSupportedAnnotationsTable(DynamoDbClient dynamoDB, String tableName) throws InterruptedException { + createTestTable(dynamoDB, DEFAULT_PROVISIONED_THROUGHPUT, + tableName, + "id" /* hashKeyName */, + ScalarAttributeType.S, + "rangeKey" /* rangeKeyName */, + ScalarAttributeType.S); + } + + /** + * Helper method to create a table in Amazon DynamoDB. + * Waits till the table becomes active. + * + * rangeKeyName and rangeKeyAttributeType are optional. + * + */ + public static void createTestTable(DynamoDbClient dynamoDB, + ProvisionedThroughput provisionedThroughput, + String tableName, + String hashKeyName, + ScalarAttributeType hashKeyAttributeType, + String rangeKeyName, + ScalarAttributeType rangeKeyAttributeType) throws InterruptedException { + List keySchema = new ArrayList(); + List attributeDefinitions = new ArrayList(); + + keySchema.add(KeySchemaElement.builder().attributeName(hashKeyName).keyType(KeyType.HASH).build()); + attributeDefinitions.add( + AttributeDefinition.builder().attributeName(hashKeyName).attributeType(hashKeyAttributeType).build()); + + if (rangeKeyName != null) { + keySchema.add(KeySchemaElement.builder().attributeName(rangeKeyName).keyType(KeyType.RANGE).build()); + attributeDefinitions.add( + AttributeDefinition.builder().attributeName(rangeKeyName).attributeType(rangeKeyAttributeType).build()); + } + + CreateTableRequest createTableRequest = CreateTableRequest.builder() + .tableName(tableName) + .keySchema(keySchema) + .attributeDefinitions(attributeDefinitions) + .provisionedThroughput(provisionedThroughput) + .build(); + + if (createTableIfNotExists(dynamoDB, createTableRequest)) { + dynamoDB.waiter().waitUntilTableExists(b -> b.tableName(tableName)); + } + } + + private static boolean createTableIfNotExists(DynamoDbClient dynamoDB, CreateTableRequest createTableRequest) { + try { + dynamoDB.createTable(createTableRequest); + return true; + } catch (software.amazon.awssdk.services.dynamodb.model.ResourceInUseException e) { + // Table already exists. + return false; + } + } + + /** + * Helper method to delete a table in Amazon DynamoDB. + * Waits till the table becomes deleted or transitions to DELETING state. + */ + public static void deleteTestTable(DynamoDbClient dynamoDB, String tableName) throws InterruptedException { + DeleteTableRequest deleteTableRequest = DeleteTableRequest.builder().tableName(tableName).build(); + + try { + dynamoDB.deleteTable(deleteTableRequest); + waitUntilTableDeletedOrInDeleting(dynamoDB, tableName); + } catch (ResourceNotFoundException e) { + // Table does not exist; nothing to delete. + } + } + + private static void waitUntilTableDeletedOrInDeleting(DynamoDbClient dynamoDB, String tableName) throws InterruptedException { + long startTime = System.currentTimeMillis(); + // Wait up to one minute for a table to be deleted or transition to DELETING state. + long endTime = startTime + 60 * 1000; + while (System.currentTimeMillis() < endTime) { + try { + DescribeTableResponse describeTableResult = + dynamoDB.describeTable(b -> b.tableName(tableName)); + if (describeTableResult.table().tableStatus() == TableStatus.DELETING) { + // Table will eventually get deleted, stopping to wait here reduces test run time by more than half + return; + } + Thread.sleep(1000); + } catch (ResourceNotFoundException e) { + return; + } + } + } + + // Denotes the number of a given type of operations in a transactionWriteRequest + // having condition expressions + public enum ConditionExpressionMode { + NONE, // None of the items have condition expressions. + ONE, // Only one item has condition expression with neither attributeName map nor attributeValue map, a simple condition expression + ONE_ATTRIBUTE_NAMES_ONLY, // One item has condition expression with attributeName map but no attributeValue map, other item has simple condition expression + ONE_ATTRIBUTE_VALUES_ONLY, // One item has condition expression with no attributeName map but with attributeValue map, other item has simple condition expression + ONE_ATTRIBUTE_NAMES_VALUES, // One item has condition expression with attributeName map and with attributeValue map, other item has simple condition expression + ONE_INVALID_RETURN_VALUES_SET, // One item has condition expression that has failing condition expression, other item has simple condition expression + ONE_RESERVED_WORD, // only one item has condition expression with restricted word + MULTIPLE, // Multiple items have condition expression with neither attributeName map nor attributeValue map + MULTIPLE_ATTRIBUTE_NAMES_ONLY, // Multiple items have condition expression with attributeName map but no attributeValue map + MULTIPLE_ATTRIBUTE_VALUES_ONLY, // Multiple items have condition expression with no attributeName map but with attributeValue map + MULTIPLE_ATTRIBUTE_NAMES_VALUES, // Multiple items have condition expression with attributeName map and with attributeValue map + MULTIPLE_INVALID_RETURN_VALUES_SET // Multiple items have condition expression that has failing condition expression + } + + public static class TransactionWriteTestRequest { + private ConditionExpressionMode putConditionExpressionMode = ConditionExpressionMode.NONE; + private ConditionExpressionMode updateConditionExpressionMode = ConditionExpressionMode.NONE; + private ConditionExpressionMode deleteConditionExpressionMode = ConditionExpressionMode.NONE; + private ConditionExpressionMode conditionCheckConditionExpressionMode = ConditionExpressionMode.MULTIPLE; + private boolean isMultiTable = true; // Majority of tests depend on objects from multiple tables + + public TransactionWriteTestRequest withPutConditionExpressionMode(ConditionExpressionMode putConditionExpressionMode) { + this.putConditionExpressionMode = putConditionExpressionMode; + return this; + } + + public TransactionWriteTestRequest withUpdateConditionExpressionMode(ConditionExpressionMode updateConditionExpressionMode) { + this.updateConditionExpressionMode = updateConditionExpressionMode; + return this; + } + + public TransactionWriteTestRequest withDeleteConditionExpressionMode(ConditionExpressionMode deleteConditionExpressionMode) { + this.deleteConditionExpressionMode = deleteConditionExpressionMode; + return this; + } + + public TransactionWriteTestRequest withConditionCheckConditionExpressionMode(ConditionExpressionMode conditionCheckConditionExpressionMode) { + this.conditionCheckConditionExpressionMode = conditionCheckConditionExpressionMode; + return this; + } + + public TransactionWriteTestRequest withIsMultiTable(boolean isMultiTable) { + this.isMultiTable = isMultiTable; + return this; + } + + } + + public static class TransactionWriteTestData { + private TransactionWriteRequest transactionWriteRequest; + private List expectedObjectKeys; + private List expectedObjects; + private List oldExistingObjects; + + public TransactionWriteTestData(TransactionWriteRequest transactionWriteRequest, + List expectedObjectKeys, + List expectedObjects, + List oldExistingObjects) { + this.transactionWriteRequest = transactionWriteRequest; + this.expectedObjectKeys = expectedObjectKeys; + this.expectedObjects = expectedObjects; + this.oldExistingObjects = oldExistingObjects; + } + + public TransactionWriteRequest getTransactionWriteRequest() { + return transactionWriteRequest; + } + + public List getExpectedObjectKeys() { + return expectedObjectKeys; + } + + public List getExpectedObjects() { + return expectedObjects; + } + + public List getOldExistingObjects() { + return oldExistingObjects; + } + + } + + public static TransactionWriteTestData createMultipleObjectTestDataForTransactionWrite(DynamoDBMapper dynamoDBMapper, + TransactionWriteTestRequest transactionWriteTestRequest) { + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + List responseObjectKeys = new ArrayList(); + List expectedResponseObjects = new ArrayList(); + List oldExistingObjects = new ArrayList(); + WriteExpressionAndReturnValue writeExpressionAndReturnValue; + + // Create and add Put operations + for (int i = 0; i < 2; i++) { + Object obj = getTestObject(transactionWriteTestRequest.isMultiTable, i); + if (!transactionWriteTestRequest.putConditionExpressionMode.equals(ConditionExpressionMode.NONE)) { + dynamoDBMapper.save(obj); + oldExistingObjects.add(obj); + } + writeExpressionAndReturnValue = generateWriteExpressionAndReturnValueFlag(obj, transactionWriteTestRequest.putConditionExpressionMode, i); + transactionWriteRequest.addPut(obj, writeExpressionAndReturnValue.dynamoDBTransactionWriteExpression, writeExpressionAndReturnValue.returnValuesOnConditionCheckFailure); + responseObjectKeys.add(obj); + expectedResponseObjects.add(obj); + } + + // Create and add update operations + for (int i = 0; i < 2; i++) { + Object obj = getTestObject(transactionWriteTestRequest.isMultiTable, i); + writeExpressionAndReturnValue = generateWriteExpressionAndReturnValueFlag(obj, transactionWriteTestRequest.updateConditionExpressionMode, i); + dynamoDBMapper.save(obj); + // Make a copy since update item updates the object. + Object newObj = copyTransactWriteItemTestObject(obj); + oldExistingObjects.add(newObj); + updateUpdateObject(obj); + transactionWriteRequest.addUpdate(obj, writeExpressionAndReturnValue.dynamoDBTransactionWriteExpression, writeExpressionAndReturnValue.returnValuesOnConditionCheckFailure); + responseObjectKeys.add(obj); + expectedResponseObjects.add(obj); + } + + // Create and add conditionCheck + Object obj = getTestObject(transactionWriteTestRequest.isMultiTable, 0); + writeExpressionAndReturnValue = generateWriteExpressionAndReturnValueFlag(obj, transactionWriteTestRequest.conditionCheckConditionExpressionMode, 0); + dynamoDBMapper.save(obj); + oldExistingObjects.add(obj); + transactionWriteRequest.addConditionCheck(obj, writeExpressionAndReturnValue.dynamoDBTransactionWriteExpression, writeExpressionAndReturnValue.returnValuesOnConditionCheckFailure); + + // Create and add delete + obj = getTestObject(transactionWriteTestRequest.isMultiTable, 0); + writeExpressionAndReturnValue = generateWriteExpressionAndReturnValueFlag(obj, transactionWriteTestRequest.deleteConditionExpressionMode, 0); + dynamoDBMapper.save(obj); + transactionWriteRequest.addDelete(obj, writeExpressionAndReturnValue.dynamoDBTransactionWriteExpression, writeExpressionAndReturnValue.returnValuesOnConditionCheckFailure); + responseObjectKeys.add(obj); + oldExistingObjects.add(obj); + expectedResponseObjects.add(null); + + return new TransactionWriteTestData(transactionWriteRequest, responseObjectKeys, expectedResponseObjects, oldExistingObjects); + } + + private static class WriteExpressionAndReturnValue { + DynamoDBTransactionWriteExpression dynamoDBTransactionWriteExpression; + ReturnValuesOnConditionCheckFailure returnValuesOnConditionCheckFailure; + + public WriteExpressionAndReturnValue(DynamoDBTransactionWriteExpression dynamoDBTransactionWriteExpression, + ReturnValuesOnConditionCheckFailure returnValuesOnConditionCheckFailure) { + this.dynamoDBTransactionWriteExpression = dynamoDBTransactionWriteExpression; + this.returnValuesOnConditionCheckFailure = returnValuesOnConditionCheckFailure; + } + } + + private static void updateUpdateObject(Object obj) { + Class clazz = obj.getClass(); + String updatedStringAttribute = "updatedStringAttribute"; + if (clazz.equals(StringAttributeClass.class)) { + ((StringAttributeClass) obj).setStringAttribute(updatedStringAttribute); + } else if (clazz.equals(TestItem.class)) { + ((TestItem) obj).setStringAttribute(updatedStringAttribute); + } else if (clazz.equals(HashKeyRangeKeyClass.class)) { + ((HashKeyRangeKeyClass) obj).setStringAttribute(updatedStringAttribute); + } else { + throw new IllegalArgumentException("Received a class that is not supported for request generation, class: " + clazz); + } + } + + private static WriteExpressionAndReturnValue generateWriteExpressionAndReturnValueFlag(Object obj, + ConditionExpressionMode conditionExpressionMode, + int tableNumber) { + Class clazz = obj.getClass(); + String stringAttribute; + DynamoDBTransactionWriteExpression dynamoDBTransactionWriteExpression = null; + ReturnValuesOnConditionCheckFailure returnValuesOnConditionCheckFailure = null; + if (clazz.equals(StringAttributeClass.class)) { + stringAttribute = ((StringAttributeClass) obj).getStringAttribute(); + } else if (clazz.equals(TestItem.class)) { + stringAttribute = ((TestItem) obj).getStringAttribute(); + } else if (clazz.equals(HashKeyRangeKeyClass.class)) { + stringAttribute = ((HashKeyRangeKeyClass) obj).getStringAttribute(); + } else { + throw new IllegalArgumentException("Received a class that is not supported for request generation, class: " + clazz); + } + switch (conditionExpressionMode) { + case NONE: + break; + case ONE: + if (tableNumber % 3 == 0) { + dynamoDBTransactionWriteExpression = generateStringAttributeIfExistsTransactWriteExpression(); + } + break; + case MULTIPLE: + if (tableNumber % 3 == 0 || tableNumber % 3 == 1) { + dynamoDBTransactionWriteExpression = generateStringAttributeIfExistsTransactWriteExpression(); + } + break; + case ONE_ATTRIBUTE_NAMES_VALUES: + if (tableNumber % 3 == 0) { + dynamoDBTransactionWriteExpression = generateStringMatcherTransactWriteExpression(stringAttribute, NON_MATCHING_STRING); + } else if (tableNumber % 3 == 1) { + dynamoDBTransactionWriteExpression = generateStringAttributeIfExistsTransactWriteExpression(); + } + break; + case MULTIPLE_ATTRIBUTE_NAMES_VALUES: + if (tableNumber % 3 == 0 || tableNumber % 3 == 1) { + dynamoDBTransactionWriteExpression = generateStringMatcherTransactWriteExpression(stringAttribute, NON_MATCHING_STRING); + } + break; + case ONE_ATTRIBUTE_NAMES_ONLY: + if (tableNumber % 3 == 0) { + dynamoDBTransactionWriteExpression = generateStringAttributeIfExistsWithExpressionAttributeNameTransactWriteExpression(); + } else if (tableNumber % 3 == 1) { + dynamoDBTransactionWriteExpression = generateStringAttributeIfExistsTransactWriteExpression(); + } + break; + case ONE_ATTRIBUTE_VALUES_ONLY: + if (tableNumber % 3 == 0) { + dynamoDBTransactionWriteExpression = generateStringContainsWithExpressionAttributeValuesTransactWriteExpression(stringAttribute.substring(2)); + } else if (tableNumber % 3 == 1) { + dynamoDBTransactionWriteExpression = generateStringAttributeIfExistsTransactWriteExpression(); + } + break; + case MULTIPLE_ATTRIBUTE_NAMES_ONLY: + if (tableNumber % 3 == 0 || tableNumber % 3 == 1) { + dynamoDBTransactionWriteExpression = generateStringAttributeIfExistsWithExpressionAttributeNameTransactWriteExpression(); + } + break; + case MULTIPLE_ATTRIBUTE_VALUES_ONLY: + if (tableNumber % 3 == 0 || tableNumber % 3 == 1) { + dynamoDBTransactionWriteExpression = generateStringContainsWithExpressionAttributeValuesTransactWriteExpression(stringAttribute.substring(2)); + } + break; + case ONE_INVALID_RETURN_VALUES_SET: + if (tableNumber % 3 == 0) { + dynamoDBTransactionWriteExpression = generateStringContainsTransactWriteExpression(stringAttribute.substring(2) + NON_MATCHING_STRING); + returnValuesOnConditionCheckFailure = ReturnValuesOnConditionCheckFailure.ALL_OLD; + } else if (tableNumber % 3 == 1) { + dynamoDBTransactionWriteExpression = generateStringAttributeIfExistsTransactWriteExpression(); + } + break; + case MULTIPLE_INVALID_RETURN_VALUES_SET: + if (tableNumber % 3 == 0 || tableNumber % 3 == 1) { + dynamoDBTransactionWriteExpression = generateStringContainsTransactWriteExpression(stringAttribute.substring(2) + NON_MATCHING_STRING); + returnValuesOnConditionCheckFailure = ReturnValuesOnConditionCheckFailure.ALL_OLD; + } + break; + case ONE_RESERVED_WORD: + if (tableNumber % 3 == 0) { + dynamoDBTransactionWriteExpression = generateTransactWriteExpressionContainingReservedWord(); + } + break; + default: + throw new IllegalArgumentException("Invalid expression mode: " + conditionExpressionMode + " passed in"); + } + return new WriteExpressionAndReturnValue(dynamoDBTransactionWriteExpression, returnValuesOnConditionCheckFailure); + } + + // Generates a write condition expression that'll check if either + // matchingString1 or matchingString2 is exact match of *stringAttribute*'s value + public static DynamoDBTransactionWriteExpression generateStringMatcherTransactWriteExpression(String matchingString1, + String matchingString2) { + DynamoDBTransactionWriteExpression writeExpression = new DynamoDBTransactionWriteExpression(); + Map attributeNameMap = new HashMap(); + attributeNameMap.put("#sA", "stringAttribute"); + writeExpression.withConditionExpression("(#sA IN (:attr1, :attr2))"); + Map attributeValueMap = new HashMap(); + attributeValueMap.put(":attr1", AttributeValue.builder().s(matchingString1).build()); + attributeValueMap.put(":attr2", AttributeValue.builder().s(matchingString2).build()); + writeExpression.withExpressionAttributeValues(attributeValueMap); + writeExpression.withExpressionAttributeNames(attributeNameMap); + return writeExpression; + } + + // Generates a write condition expression that'll check if matchingSubString is *stringAttribute* value's substring + public static DynamoDBTransactionWriteExpression generateStringContainsTransactWriteExpression(String matchingSubString) { + DynamoDBTransactionWriteExpression writeExpression = new DynamoDBTransactionWriteExpression(); + Map attributeNameMap = new HashMap(); + attributeNameMap.put("#sA", "stringAttribute"); + writeExpression.withConditionExpression("contains(#sA, :attrV)"); + Map attributeValueMap = new HashMap(); + attributeValueMap.put(":attrV", AttributeValue.builder().s(matchingSubString).build()); + writeExpression.withExpressionAttributeValues(attributeValueMap); + writeExpression.withExpressionAttributeNames(attributeNameMap); + return writeExpression; + } + + // Generates a write condition expression that'll check if matchingSubString is *stringAttribute* value's substring + // Uses only ExpressionAttributeNames + public static DynamoDBTransactionWriteExpression generateStringAttributeIfExistsWithExpressionAttributeNameTransactWriteExpression() { + DynamoDBTransactionWriteExpression writeExpression = new DynamoDBTransactionWriteExpression(); + Map attributeNameMap = new HashMap(); + attributeNameMap.put("#sA", "stringAttribute"); + writeExpression.withConditionExpression("attribute_exists(#sA)"); + writeExpression.withExpressionAttributeNames(attributeNameMap); + return writeExpression; + } + + // Generates a write condition expression that'll check if subStringToMatch is *stringAttribute* value's substring + // Uses only ExpressionAttributeValues + public static DynamoDBTransactionWriteExpression generateStringContainsWithExpressionAttributeValuesTransactWriteExpression(String subStringToMatch) { + DynamoDBTransactionWriteExpression writeExpression = new DynamoDBTransactionWriteExpression(); + writeExpression.withConditionExpression("contains(stringAttribute, :attrV)"); + Map attributeValueMap = new HashMap(); + attributeValueMap.put(":attrV", AttributeValue.builder().s(subStringToMatch).build()); + writeExpression.withExpressionAttributeValues(attributeValueMap); + return writeExpression; + } + + public static DynamoDBTransactionWriteExpression generateStringAttributeIfExistsTransactWriteExpression() { + DynamoDBTransactionWriteExpression writeExpression = new DynamoDBTransactionWriteExpression(); + writeExpression.withConditionExpression("attribute_exists(stringAttribute)"); + return writeExpression; + } + + public static DynamoDBTransactionWriteExpression generateTransactWriteExpressionContainingReservedWord() { + DynamoDBTransactionWriteExpression writeExpression = new DynamoDBTransactionWriteExpression(); + writeExpression.withConditionExpression("attribute_exists(ABORT)"); + return writeExpression; + } + + private static Object copyTransactWriteItemTestObject(Object obj) { + if (obj.getClass().equals(StringAttributeClass.class)) { + StringAttributeClass stringAttributeClassObject = (StringAttributeClass) obj; + StringAttributeClass stringAttributeClassNewObject = new StringAttributeClass(); + stringAttributeClassNewObject.setRenamedAttribute(stringAttributeClassObject.getRenamedAttribute()); + stringAttributeClassNewObject.setStringAttribute(stringAttributeClassObject.getStringAttribute()); + stringAttributeClassNewObject.setKey(stringAttributeClassObject.getKey()); + return stringAttributeClassNewObject; + } else if (obj.getClass().equals(TestItem.class)) { + TestItem testItemObject = (TestItem) obj; + TestItem testItemNewObject = new TestItem(); + testItemNewObject.setHashKey(testItemObject.getHashKey()); + testItemNewObject.setRangeKey(testItemObject.getRangeKey()); + testItemNewObject.setStringAttribute(testItemObject.getStringAttribute()); + testItemNewObject.setNonKeyAttribute(testItemObject.getNonKeyAttribute()); + testItemNewObject.setStringSetAttribute(testItemObject.getStringSetAttribute()); + return testItemNewObject; + } else if (obj.getClass().equals(HashKeyRangeKeyClass.class)) { + HashKeyRangeKeyClass hashKeyRangeKeyClassObject = (HashKeyRangeKeyClass) obj; + HashKeyRangeKeyClass hashKeyRangeKeyClassNewObject = new HashKeyRangeKeyClass(); + hashKeyRangeKeyClassNewObject.setHashKey(hashKeyRangeKeyClassObject.getHashKey()); + hashKeyRangeKeyClassNewObject.setRangeKey(hashKeyRangeKeyClassObject.getRangeKey()); + hashKeyRangeKeyClassNewObject.setStringAttribute(hashKeyRangeKeyClassObject.getStringAttribute()); + return hashKeyRangeKeyClassNewObject; + } else { + throw new IllegalArgumentException("Unsupported class passed in: " + obj.getClass()); + } + } + + // Replaces the operation at index in transactionWriteOperations with newObject + public static void updateTransactionWriteOperationsWithNewObject(List transactionWriteOperations, + Object newObject, + int index) { + TransactionWriteOperation oldOperation = transactionWriteOperations.get(index); + updateTransactionWriteOperationsWithNewObjectAndExpression(transactionWriteOperations, + newObject, + oldOperation.getDynamoDBTransactionWriteExpression(), + index); + } + + + // Replaces the operation at index in transactionWriteOperations with newObject and newDynamoDBTransactionWriteExpression + public static void updateTransactionWriteOperationsWithNewObjectAndExpression(List transactionWriteOperations, + Object newObject, + DynamoDBTransactionWriteExpression newDynamoDBTransactionWriteExpression, + int index) { + TransactionWriteOperation oldOperation = transactionWriteOperations.get(index); + TransactionWriteOperation newTransactionWriteOperation = new TransactionWriteOperation(newObject, + oldOperation.getTransactionWriteOperationType(), + newDynamoDBTransactionWriteExpression, + oldOperation.getReturnValuesOnConditionCheckFailure()); + transactionWriteOperations.remove(index); + transactionWriteOperations.add(index, newTransactionWriteOperation); + } + + public static List extractAllObjectsFromTransactionWriteOperations(List transactionWriteOperations) { + List objectList = new ArrayList(); + for (TransactionWriteOperation transactionWriteOperation : transactionWriteOperations) { + objectList.add(transactionWriteOperation.getObject()); + } + return objectList; + } + + // Extracts objects that corresponding to put, update, delete. + public static List extractObjectKeysForValidResponseSendingOperations(List allExistingObjects) { + List responseObjectKeys = new ArrayList(); + for (int i = 0; i < 4; i++) { + responseObjectKeys.add(allExistingObjects.get(i)); + } + responseObjectKeys.add(allExistingObjects.get(5)); // For objectToBeDeleted + return responseObjectKeys; + } + + public static List extractExpectedResponseObjects(List allExistingObjects) { + List responseObjects = new ArrayList(); + for (int i = 0; i < 4; i++) { + responseObjects.add(allExistingObjects.get(i)); + } + responseObjects.add(null); // For objectToBeDeleted + return responseObjects; + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/TestParameters.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/TestParameters.java new file mode 100644 index 000000000000..8e2bc59ba293 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/TestParameters.java @@ -0,0 +1,62 @@ +/* + * 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.mapper.dynamodb; + +import java.util.Map; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +public class TestParameters + implements AttributeTransformer.Parameters { + + private Map attributeValues; + + public TestParameters() { + this(null); + } + + public TestParameters(Map attributeValues) { + this.attributeValues = attributeValues; + } + + public Map getAttributeValues() { + return attributeValues; + } + + public boolean isPartialUpdate() { + return false; + } + + public Class getModelClass() { + return null; + } + + public DynamoDBMapperConfig getMapperConfig() { + return null; + } + + public String getTableName() { + return null; + } + + public String getHashKeyName() { + return null; + } + + public String getRangeKeyName() { + return null; + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/TransactionLoadTableMapperUnitTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/TransactionLoadTableMapperUnitTest.java new file mode 100644 index 000000000000..97114dea99f7 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/TransactionLoadTableMapperUnitTest.java @@ -0,0 +1,86 @@ +/* + * 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.mapper.dynamodb; + +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.createSdkRangeTestTable; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.getStringAttributeClass; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.getUniqueRangeKeyObject; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import java.util.List; + +import software.amazon.awssdk.mapper.dynamodb.pojos.RangeKeyClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.StringAttributeClass; +import org.junit.BeforeClass; +import org.junit.Test; + +public class TransactionLoadTableMapperUnitTest extends TransactionsUnitTestBase { + + @BeforeClass + public static void setUpTests() throws InterruptedException { + setUp(); + dynamoMapper = new DynamoDBMapper(dynamoDB); + createSdkRangeTestTable(dynamoDB); + } + + @Test + public void testSimpleTableMapperTransactionLoadSucceeds() { + final DynamoDBTableMapper tableMapper = dynamoMapper.newTableMapper(RangeKeyClass.class); + final int numItems = 10; + TransactionLoadRequest transactionLoadRequest = new TransactionLoadRequest(); + + for ( int i = 0; i < numItems; i++ ) { + RangeKeyClass obj = getUniqueRangeKeyObject(); + tableMapper.save(obj); + transactionLoadRequest.addLoad(obj); + } + + List responseObjectList = tableMapper.transactionLoad(transactionLoadRequest); + List requestObjectList = transactionLoadRequest.getObjectsToLoad(); + for(int i = 0; i < numItems; i++) { + assertEquals(requestObjectList.get(i), responseObjectList.get(i)); + } + } + + @Test + public void testWrongInputObjectForTableMapperTransactionLoadFails() { + final DynamoDBTableMapper tableMapper = dynamoMapper.newTableMapper(RangeKeyClass.class); + TransactionLoadRequest transactionLoadRequest = new TransactionLoadRequest(); + transactionLoadRequest.addLoad(getStringAttributeClass()); + try { + tableMapper.transactionLoad(transactionLoadRequest); + fail("Expected DynamoDBMappingException but no exception thrown"); + } catch (DynamoDBMappingException dme) { + assertEquals(dme.getMessage(), "Input object is of the classType: " + StringAttributeClass.class + " but tableMapper is declared with classType: " + RangeKeyClass.class); + } + } + + @Test + public void testMixedInputsObjectForTableMapperTransactionLoadFails() { + final DynamoDBTableMapper tableMapper = dynamoMapper.newTableMapper(RangeKeyClass.class); + TransactionLoadRequest transactionLoadRequest = new TransactionLoadRequest(); + RangeKeyClass obj = getUniqueRangeKeyObject(); + transactionLoadRequest.addLoad(obj); + transactionLoadRequest.addLoad(getStringAttributeClass()); + try { + tableMapper.transactionLoad(transactionLoadRequest); + fail("Expected DynamoDBMappingException but no exception thrown"); + } catch (DynamoDBMappingException dme) { + assertEquals(dme.getMessage(), "Input object is of the classType: " + StringAttributeClass.class + " but tableMapper is declared with classType: " + RangeKeyClass.class); + } + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/TransactionLoadUnitTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/TransactionLoadUnitTest.java new file mode 100644 index 000000000000..c7c330e1e440 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/TransactionLoadUnitTest.java @@ -0,0 +1,244 @@ +/* + * 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.mapper.dynamodb; + +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.DEFAULT_PROVISIONED_THROUGHPUT; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.createSdkMapperSaveConfigTable; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.createSdkRangeTestTable; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.createStringHashKeyTable; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.createTestTable; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.getMultiTableObjects; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.getSchemaViolatingTestItemObject; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.getStringAttributeClass; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.getUniqueRangeKeyObject; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.fail; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import software.amazon.awssdk.mapper.dynamodb.mapper.NoSuchTableClass; +import software.amazon.awssdk.services.dynamodb.model.ResourceNotFoundException; +import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType; +import software.amazon.awssdk.services.dynamodb.model.TransactionCanceledException; +import software.amazon.awssdk.mapper.dynamodb.pojos.RangeKeyClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.StringAttributeClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.TestItem; +import org.junit.BeforeClass; +import org.junit.Test; + +public class TransactionLoadUnitTest extends TransactionsUnitTestBase { + + @BeforeClass + public static void setUpTests() throws InterruptedException { + setUp(); + dynamoMapper = new DynamoDBMapper(dynamoDB); + createSdkMapperSaveConfigTable(dynamoDB); + createSdkRangeTestTable(dynamoDB); + createStringHashKeyTable(dynamoDB); + } + + @Test + public void testTransactionLoadCanLoadSingleItems() { + StringAttributeClass obj = getStringAttributeClass(); + dynamoMapper.save(obj); + TransactionLoadRequest transactionLoadRequest = new TransactionLoadRequest(); + transactionLoadRequest.addLoad(obj); + List responseObjectList = dynamoMapper.transactionLoad(transactionLoadRequest, DynamoDBMapperConfig.DEFAULT); + assertEquals(1, responseObjectList.size()); + assertEquals(obj, responseObjectList.get(0)); + } + + @Test + public void testTranasctionLoadForNonExistingItemsReturnsNull() { + StringAttributeClass obj = getStringAttributeClass(); + TransactionLoadRequest transactionLoadRequest = new TransactionLoadRequest(); + transactionLoadRequest.addLoad(obj); + StringAttributeClass obj2 = getStringAttributeClass(); + transactionLoadRequest.addLoad(obj2); + List responseObjectList = dynamoMapper.transactionLoad(transactionLoadRequest, DynamoDBMapperConfig.DEFAULT); + assertEquals(2, responseObjectList.size()); + for (Object respObj : responseObjectList) { + assertEquals(null, respObj); + } + } + + @Test + public void testTransactionLoadForOverriddenConfigReturnsOverriddenTableValues() throws InterruptedException { + String overriddenTableName = "overriddenTableName"; + String stringInOverriddenTable = "StringAttributeOnOverriddenTable"; + createTestTable(dynamoDB, DEFAULT_PROVISIONED_THROUGHPUT, + overriddenTableName, + "key" /* hashKeyName */, + ScalarAttributeType.S, + null /* rangeKeyName */, + null /* rangeKeyAttributeType */); + StringAttributeClass obj = getStringAttributeClass(); + DynamoDBMapperConfig.TableNameOverride tableNameOverride = new DynamoDBMapperConfig.TableNameOverride(overriddenTableName); + + DynamoDBMapperConfig config = DynamoDBMapperConfig.builder() + .withTableNameOverride(tableNameOverride) + .build(); + dynamoMapper.save(obj); + obj.setStringAttribute(stringInOverriddenTable); + // Saving a value with overridden tableName + dynamoMapper.save(obj, config); + TransactionLoadRequest transactionLoadRequest = new TransactionLoadRequest(); + transactionLoadRequest.addLoad(obj); + List responseObjectList = dynamoMapper.transactionLoad(transactionLoadRequest, config); + assertEquals(1, responseObjectList.size()); + StringAttributeClass stringAttributeClassObject = (StringAttributeClass) responseObjectList.get(0); + assertEquals(stringInOverriddenTable, stringAttributeClassObject.getStringAttribute()); + } + + @Test + public void testTransactionLoadOnMultipleTablesSucceeds() { + + final int numItems = 10; + List objs = getMultiTableObjects(numItems); + TransactionLoadRequest transactionLoadRequest = new TransactionLoadRequest(); + for (Object obj : objs) { + dynamoMapper.save(obj); + transactionLoadRequest.addLoad(obj); + } + List responseObjectList = dynamoMapper.transactionLoad(transactionLoadRequest, DynamoDBMapperConfig.DEFAULT); + + for (int i = 0; i < numItems; i++) { + // Ensure that request and response objects are in the same order as this is a transactionLoad request + assertEquals(objs.get(i), responseObjectList.get(i)); + } + } + + @Test + public void testTransactionLoadOnMultipleTablesWithProjectionsSucceeds() throws InterruptedException { + + final int numItems = 10; + List objs = getMultiTableObjects(numItems); + TransactionLoadRequest transactionLoadRequest = new TransactionLoadRequest(); + // Read only integerAttributes from the item + Map expressionAttributeNames = new HashMap(); + expressionAttributeNames.put("#sa", "stringAttribute"); + String projectIntegerAttribute = "#sa"; + DynamoDBTransactionLoadExpression dynamoDBTransactionLoadExpression = new DynamoDBTransactionLoadExpression() + .withProjectionExpression(projectIntegerAttribute) + .withExpressionAttributeNames(expressionAttributeNames); + for (int i = 0; i < numItems; i++) { + dynamoMapper.save(objs.get(i)); + if (i % 2 == 0) { + transactionLoadRequest.addLoad(objs.get(i), dynamoDBTransactionLoadExpression); + } else { + transactionLoadRequest.addLoad(objs.get(i)); + } + } + + List responseObjectList = dynamoMapper.transactionLoad(transactionLoadRequest, DynamoDBMapperConfig.DEFAULT); + + for (int i = 0; i < numItems/2; i++) { + StringAttributeClass stringAttributeClassObject = (StringAttributeClass) responseObjectList.get(i); + if (i % 2 == 0) { + assertEquals(stringAttributeClassObject.getKey(), null); + assertNotEquals(stringAttributeClassObject.getStringAttribute(), null); + } else { + assertEquals(objs.get(i), stringAttributeClassObject); + } + } + + for (int i = numItems/2 ; i < numItems; i++) { + TestItem testItemClassObject = (TestItem) responseObjectList.get(i); + if (i % 2 == 0) { + assertEquals(testItemClassObject.getHashKey(), null); + assertNotEquals(testItemClassObject.getStringAttribute(), null); + } else { + assertEquals(objs.get(i), (testItemClassObject)); + } + } + } + + @Test + public void testTransactionLoadOnNonExistingAttributeProjectionTestSucceeds() { + StringAttributeClass obj = getStringAttributeClass(); + dynamoMapper.save(obj); + TransactionLoadRequest transactionLoadRequest = new TransactionLoadRequest(); + Map expressionAttributeNames = new HashMap(); + expressionAttributeNames.put("#nsa", "nonStringAttribute"); + String projectIntegerAttribute = "#nsa"; + DynamoDBTransactionLoadExpression dynamoDBTransactionLoadExpression = new DynamoDBTransactionLoadExpression() + .withProjectionExpression(projectIntegerAttribute) + .withExpressionAttributeNames(expressionAttributeNames); + transactionLoadRequest.addLoad(obj, dynamoDBTransactionLoadExpression); + List responseObjectList = dynamoMapper.transactionLoad(transactionLoadRequest, DynamoDBMapperConfig.DEFAULT); + assertEquals(1, responseObjectList.size()); + StringAttributeClass stringAttributeClassObject = (StringAttributeClass)responseObjectList.get(0); + assertEquals(null, stringAttributeClassObject.getKey()); + assertEquals(null, stringAttributeClassObject.getRenamedAttribute()); + assertEquals(null, stringAttributeClassObject.getStringAttribute()); + } + + @Test + public void testTransactionLoadOnVersionedAttributeSucceeds() { + + final int numItems = 10; + TransactionLoadRequest transactionLoadRequest = new TransactionLoadRequest(); + + for ( int i = 0; i < numItems; i++ ) { + RangeKeyClass obj = getUniqueRangeKeyObject(); + DynamoDBMapperTableModel model = getTable(obj); + DynamoDBMapperFieldModel val = model.field("version"); + assertEquals(true, val.versioned()); + dynamoMapper.save(obj); + transactionLoadRequest.addLoad(obj); + } + + List responseObjectList = dynamoMapper.transactionLoad(transactionLoadRequest, DynamoDBMapperConfig.DEFAULT); + for(int i = 0; i < numItems; i++) { + assertEquals(transactionLoadRequest.getObjectsToLoad().get(i), responseObjectList.get(i)); + } + } + + @Test + public void testTransactionLoadOnNonExistingTableReturnsResourceNotFoundException() { + final int numItems = 10; + TransactionLoadRequest transactionLoadRequest = new TransactionLoadRequest(); + for ( int i = 0; i < numItems; i++ ) { + NoSuchTableClass obj = new NoSuchTableClass(); + obj.setKey("someKey"); + transactionLoadRequest.addLoad(obj); + } + + try { + dynamoMapper.transactionLoad(transactionLoadRequest, DynamoDBMapperConfig.DEFAULT); + fail("Expected ResourceNotFoundException but no exception thrown"); + } catch (ResourceNotFoundException e) { + // Pass + } + } + + @Test + public void testTransactionLoadOnSchemaViolatingAttributeReturnsTransactionCanceledException() { + StringAttributeClass obj = getStringAttributeClass(); + dynamoMapper.save(obj); + TransactionLoadRequest transactionLoadRequest = new TransactionLoadRequest(); + transactionLoadRequest.addLoad(getSchemaViolatingTestItemObject()); + try { + dynamoMapper.transactionLoad(transactionLoadRequest, DynamoDBMapperConfig.DEFAULT); + fail("Expected TransactionCanceledException but no exception thrown"); + } catch (TransactionCanceledException tce) { + // Pass + } + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/TransactionWriteTableMapperUnitTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/TransactionWriteTableMapperUnitTest.java new file mode 100644 index 000000000000..7dc22a6f90e3 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/TransactionWriteTableMapperUnitTest.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.mapper.dynamodb; + +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.createStringHashKeyTable; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.getStringAttributeClass; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.getTestItemObject; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.mapper.dynamodb.pojos.StringAttributeClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.TestItem; +import org.junit.BeforeClass; +import org.junit.Test; + +public class TransactionWriteTableMapperUnitTest extends TransactionsUnitTestBase { + + @BeforeClass + public static void setUpTests() throws InterruptedException { + setUp(); + dynamoMapper = new DynamoDBMapper(dynamoDB); + createStringHashKeyTable(dynamoDB); + } + + @Test + public void testSimpleTableMapperTransactionWriteSucceeds() { + final DynamoDBTableMapper tableMapper = dynamoMapper.newTableMapper(StringAttributeClass.class); + final int numItems = 10; + List requestObjectList = new ArrayList(); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + TransactionLoadRequest transactionLoadRequest = new TransactionLoadRequest(); + for ( int i = 0; i < numItems; i++ ) { + StringAttributeClass obj = getStringAttributeClass(); + requestObjectList.add(obj); + transactionWriteRequest.addPut(obj); + transactionLoadRequest.addLoad(obj); + + } + tableMapper.transactionWrite(transactionWriteRequest); + List responseObjectList = transactionLoadObjects(transactionLoadRequest); + for(int i = 0; i < numItems; i++) { + assertEquals(requestObjectList.get(i), responseObjectList.get(i)); + } + } + + @Test + public void testMultipleItemTableMapperTransactionWriteSucceeds() { + final DynamoDBTableMapper tableMapper = dynamoMapper.newTableMapper(StringAttributeClass.class); + StringAttributeClass objectToBeUpdated = getStringAttributeClass(); + StringAttributeClass objectToBeConditionChecked = getStringAttributeClass(); + StringAttributeClass objectToBeDeleted = getStringAttributeClass(); + + // Save it so that we can update it inside transaction + tableMapper.save(objectToBeUpdated); + // Save it so that we can conditionCheck it inside transaction + tableMapper.save(objectToBeConditionChecked); + // Save it so that we can delete it inside transaction + tableMapper.save(objectToBeDeleted); + + // Save the object + StringAttributeClass objectToBePut = getStringAttributeClass(); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addPut(objectToBePut); + // Update the object + String updatedStringAttribute = "updatedStringAttribute"; + objectToBeUpdated.setStringAttribute(updatedStringAttribute); + transactionWriteRequest.addUpdate(objectToBeUpdated); + // ConditionCheck the object + DynamoDBTransactionWriteExpression conditionCheckWriteExpression = new DynamoDBTransactionWriteExpression(); + conditionCheckWriteExpression.withConditionExpression("(#sA IN (:attr1, :attr2))"); + Map attributeNameMap = new HashMap(); + attributeNameMap.put("#sA", "stringAttribute"); + Map attributeValueMap = new HashMap(); + attributeValueMap.put(":attr1", AttributeValue.builder().s(objectToBeConditionChecked.getStringAttribute()).build()); + attributeValueMap.put(":attr2", AttributeValue.builder().s("not" + updatedStringAttribute).build()); + conditionCheckWriteExpression.withExpressionAttributeValues(attributeValueMap); + conditionCheckWriteExpression.withExpressionAttributeNames(attributeNameMap); + transactionWriteRequest.addConditionCheck(objectToBeConditionChecked, conditionCheckWriteExpression); + // Delete the object + StringAttributeClass objectToBeDeletedKey = new StringAttributeClass(); + objectToBeDeletedKey.setKey(objectToBeDeleted.getKey()); + + transactionWriteRequest.addDelete(objectToBeDeletedKey); + + // Ensure transactWrite succeeds implying conditionCheck succeeds + tableMapper.transactionWrite(transactionWriteRequest); + + // Ensure toPut object is put + assertEquals(objectToBePut, dynamoMapper.load(objectToBePut)); + // Ensure toUpdate object is updated + assertEquals(updatedStringAttribute, dynamoMapper.load(objectToBeUpdated).getStringAttribute()); + // Ensure toDelete object is deleted + assertEquals(null, dynamoMapper.load(objectToBeDeleted)); + } + + @Test + public void testWrongInputObjectForTableMapperTransactionWriteFails() { + final DynamoDBTableMapper tableMapper = dynamoMapper.newTableMapper(StringAttributeClass.class); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addPut(getTestItemObject()); + try { + tableMapper.transactionWrite(transactionWriteRequest); + fail("Expected DynamoDBMappingException but no exception thrown"); + } catch (DynamoDBMappingException dme) { + assertEquals(dme.getMessage(), "Input object is of the classType: " + TestItem.class + " but tableMapper is declared with classType: " + StringAttributeClass.class); + } + } + + @Test + public void testMixedInputsObjectForTableMapperTransactionLoadFails() { + final DynamoDBTableMapper tableMapper = dynamoMapper.newTableMapper(StringAttributeClass.class); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addPut(getStringAttributeClass()); + transactionWriteRequest.addDelete(getTestItemObject()); + try { + tableMapper.transactionWrite(transactionWriteRequest); + fail("Expected DynamoDBMappingException but no exception thrown"); + } catch (DynamoDBMappingException dme) { + assertEquals(dme.getMessage(), "Input object is of the classType: " + TestItem.class + " but tableMapper is declared with classType: " + StringAttributeClass.class); + } + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/TransactionWriteUnitTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/TransactionWriteUnitTest.java new file mode 100644 index 000000000000..56b2887100eb --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/TransactionWriteUnitTest.java @@ -0,0 +1,866 @@ +/* + * 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.mapper.dynamodb; + +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.DEFAULT_PROVISIONED_THROUGHPUT; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.createSdkDocAttributeTable; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.createSdkMapperSaveConfigTable; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.createSdkRangeTestTable; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.createSdkStringRangeTable; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.createStringHashKeyTable; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.createTestTable; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.getMultiTableObjects; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.getMultiVersionRangeKeyObject; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.getStringAttributeClass; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.getTestItemObject; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.getUniqueRangeKeyObject; +import static junit.framework.TestCase.fail; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.mapper.dynamodb.mapper.NoSuchTableClass; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; +import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; +import software.amazon.awssdk.services.dynamodb.model.ResourceNotFoundException; +import software.amazon.awssdk.services.dynamodb.model.ReturnValuesOnConditionCheckFailure; +import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType; +import software.amazon.awssdk.services.dynamodb.model.TransactionCanceledException; +import software.amazon.awssdk.mapper.dynamodb.pojos.HashKeyRangeKeyBothAutoGenerated; +import software.amazon.awssdk.mapper.dynamodb.pojos.MultiVersionRangeKeyClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.PrimaryKeysNotAutogenerated_IndexKeysAutogenerated; +import software.amazon.awssdk.mapper.dynamodb.pojos.RangeKeyClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.StringAttributeClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.TestDocClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.TestItem; +import software.amazon.awssdk.mapper.dynamodb.utils.AttributeAdder; +import software.amazon.awssdk.mapper.dynamodb.utils.NullAttributeAdder; +import org.junit.BeforeClass; +import org.junit.Test; + +import static software.amazon.awssdk.mapper.dynamodb.pojos.TestDocClass.ChildClass; +import static org.junit.Assert.assertTrue; + +public class TransactionWriteUnitTest extends TransactionsUnitTestBase { + + // Some arbitrary big integer + private static final int ATTRIBUTE_ADDER_START = 12345678; + private static final String ADDITIONAL_ATTRIBUTE_NAME = "extra_attribute"; + private static final String VERSION_CONDITION_CONFLICT_ERROR_MESSAGE = + "A transactional write operation may not also specify a condition " + + "expression if a versioned attribute is present on the " + + "model of the item."; + + @BeforeClass + public static void setUpTests() throws InterruptedException { + setUp(); + dynamoMapper = new DynamoDBMapper(dynamoDB); + createSdkMapperSaveConfigTable(dynamoDB); + createSdkRangeTestTable(dynamoDB); + createStringHashKeyTable(dynamoDB); + createSdkStringRangeTable(dynamoDB); + createSdkDocAttributeTable(dynamoDB); + } + + @Test + public void testTransactionWriteEmptyRequest() { + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + try { + dynamoMapper.transactionWrite(transactionWriteRequest); + fail("Expected SdkClientException but no exception thrown"); + } catch (SdkClientException sce) { + // Pass + } + } + + @Test + public void testTransactionWriteSingleItemTest() { + StringAttributeClass obj = getStringAttributeClass(); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addPut(obj); + + // Put one object + executeTransactWriteAndVerify(transactionWriteRequest, obj, obj); + + // Update the object + String updatedStringAttribute = "updatedStringAttribute"; + transactionWriteRequest = new TransactionWriteRequest(); + obj.setStringAttribute(updatedStringAttribute); + transactionWriteRequest.addUpdate(obj); + dynamoMapper.transactionWrite(transactionWriteRequest); + executeTransactWriteAndVerify(transactionWriteRequest, obj, obj); + + // ConditionCheck the object + transactionWriteRequest = new TransactionWriteRequest(); + DynamoDBTransactionWriteExpression writeExpression = generateStringMatcherTransactWriteExpression(updatedStringAttribute, + "not" + updatedStringAttribute); + transactionWriteRequest.addConditionCheck(obj, writeExpression); + // Succeeds + dynamoMapper.transactionWrite(transactionWriteRequest); + // Fails, assert on old returned values in exception + transactionWriteRequest = new TransactionWriteRequest(); + writeExpression = generateStringMatcherTransactWriteExpression("not1" + updatedStringAttribute, + "not2" + updatedStringAttribute); + Map expectedResponseValueMap = new HashMap(); + expectedResponseValueMap.put("originalName", AttributeValue.builder().s(obj.getRenamedAttribute()).build()); + expectedResponseValueMap.put("key", AttributeValue.builder().s(obj.getKey()).build()); + expectedResponseValueMap.put("stringAttribute", AttributeValue.builder().s(obj.getStringAttribute()).build()); + transactionWriteRequest.addConditionCheck(obj, writeExpression, ReturnValuesOnConditionCheckFailure.ALL_OLD); + try { + dynamoMapper.transactionWrite(transactionWriteRequest); + fail("Expected TransactionCanceledException but no exception thrown"); + } catch (TransactionCanceledException tce) { + // Expected + } + + // Delete + transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addDelete(obj); + executeTransactWriteAndVerify(transactionWriteRequest, obj, null); + } + + @Test + public void testTransactionWriteForOverriddenConfigWritesIntoOverriddenTable() throws InterruptedException { + String overriddenTableName = "overriddenTableName"; + String stringInOverriddenTable = "StringAttributeOnOverriddenTable"; + createTestTable(dynamoDB, DEFAULT_PROVISIONED_THROUGHPUT, + overriddenTableName, + "key" /* hashKeyName */, + ScalarAttributeType.S, + null /* rangeKeyName */, + null /* rangeKeyAttributeType */); + StringAttributeClass obj = getStringAttributeClass(); + String stringInOriginalTable = obj.getStringAttribute(); + + // Saving a value with original table + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addPut(obj); + dynamoMapper.transactionWrite(transactionWriteRequest); + + // Saving an overridden value with overridden tableName + DynamoDBMapperConfig.TableNameOverride tableNameOverride = new DynamoDBMapperConfig.TableNameOverride(overriddenTableName); + obj.setStringAttribute(stringInOverriddenTable); + DynamoDBMapperConfig config = DynamoDBMapperConfig.builder() + .withTableNameOverride(tableNameOverride) + .build(); + transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addPut(obj); + dynamoMapper.transactionWrite(transactionWriteRequest, config); + + // Query overridden table to see overridden string attribute + TransactionLoadRequest transactionLoadRequest = new TransactionLoadRequest(); + transactionLoadRequest.addLoad(obj); + List responseObjectList = transactionLoadObjects(transactionLoadRequest, config); + assertEquals(1, responseObjectList.size()); + StringAttributeClass stringAttributeClassObject = (StringAttributeClass) responseObjectList.get(0); + assertEquals(stringInOverriddenTable, stringAttributeClassObject.getStringAttribute()); + + // Query original table to see original string attribute + transactionLoadRequest = new TransactionLoadRequest(); + transactionLoadRequest.addLoad(obj); + responseObjectList = transactionLoadObjects(transactionLoadRequest); + assertEquals(1, responseObjectList.size()); + stringAttributeClassObject = (StringAttributeClass) responseObjectList.get(0); + assertEquals(stringInOriginalTable, stringAttributeClassObject.getStringAttribute()); + } + + @Test + public void testPutsViaTransactionForMultipleItemsOnMultipleTablesSucceeds() { + final int numItems = 10; + List objs = getMultiTableObjects(numItems); + TransactionLoadRequest transactionLoadRequest = new TransactionLoadRequest(); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + for (Object obj : objs) { + transactionWriteRequest.addPut(obj); + transactionLoadRequest.addLoad(obj); + } + dynamoMapper.transactionWrite(transactionWriteRequest); + List responseObjectList = transactionLoadObjects(transactionLoadRequest); + + for (int i = 0; i < numItems; i++) { + // Ensure that request and response objects are in the same order as this is a transactionLoad request + assertEquals(objs.get(i), responseObjectList.get(i)); + } + } + + @Test + public void testTransactionWriteForAutoGeneratedKeysSucceeds() { + HashKeyRangeKeyBothAutoGenerated obj = new HashKeyRangeKeyBothAutoGenerated(); + obj.setOtherAttribute("someAttributeValue"); + assertNull(obj.getKey()); + assertNull(obj.getRangeKey()); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addPut(obj); + executeTransactWriteAndVerify(transactionWriteRequest, obj, obj); + // Check in-memory value + assertNotNull(obj.getKey()); + assertNotNull(obj.getRangeKey()); + } + + @Test + public void testTransactionWriteForAutoGenerateIndexKeysSucceeds() { + PrimaryKeysNotAutogenerated_IndexKeysAutogenerated obj = new PrimaryKeysNotAutogenerated_IndexKeysAutogenerated(); + + String randomPrimaryKeyValue = UUID.randomUUID().toString(); + obj.setKey(randomPrimaryKeyValue); + obj.setRangeKey(randomPrimaryKeyValue); + + assertNull(obj.getGsiHashKey()); + assertNull(obj.getGsiRangeKey()); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addPut(obj); + executeTransactWriteAndVerify(transactionWriteRequest, obj, obj); + // check in-memory value + assertNotNull(obj.getGsiHashKey()); + assertNotNull(obj.getGsiRangeKey()); + } + + @Test + public void testTransactionWriteForAutoGenerateKeysFails() { + PrimaryKeysNotAutogenerated_IndexKeysAutogenerated obj = new PrimaryKeysNotAutogenerated_IndexKeysAutogenerated(); + assertNull(obj.getGsiHashKey()); + assertNull(obj.getGsiRangeKey()); + + // Setting rangeKey to non-null value makes this test deterministic, otherwise the exception message assert makes this test flaky because + // hash key would get validated first is NOT guaranteed + obj.setRangeKey("someRangeKey"); + + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addPut(obj); + try { + dynamoMapper.transactionWrite(transactionWriteRequest); + fail("Expected DynamoDBMappingException but no exception thrown"); + } catch (DynamoDBMappingException ase) { + assertEquals("PrimaryKeysNotAutogenerated_IndexKeysAutogenerated[key]; null or empty value for primary key", ase.getMessage()); + } + // Ensure in-memory values are not updated on failed call + assertNull(obj.getGsiHashKey()); + assertNull(obj.getGsiRangeKey()); + } + + @Test + public void testTransactionWriteForVersionedAttributeWithConditionExpressionFails() { + RangeKeyClass obj = getUniqueRangeKeyObject(); + DynamoDBMapperTableModel model = getTable(obj); + DynamoDBMapperFieldModel val = model.field("version"); + assertEquals(true, val.versioned()); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + DynamoDBTransactionWriteExpression writeExpression = new DynamoDBTransactionWriteExpression(); + writeExpression.withConditionExpression("SomeConditionExpression"); + transactionWriteRequest.addPut(obj, writeExpression); + try { + dynamoMapper.transactionWrite(transactionWriteRequest); + fail("Expected SdkClientException but no exception thrown"); + } catch (SdkClientException se) { + assertEquals(se.getMessage(), VERSION_CONDITION_CONFLICT_ERROR_MESSAGE); + } + } + + @Test + public void testTransactionWriteForNullVersionAttributeSucceeds() { + RangeKeyClass obj = getUniqueRangeKeyObject(); + DynamoDBMapperTableModel model = getTable(obj); + DynamoDBMapperFieldModel val = model.field("version"); + assertEquals(true, val.versioned()); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + assertEquals(null, obj.getVersion()); + transactionWriteRequest.addPut(obj); + dynamoMapper.transactionWrite(transactionWriteRequest); + // Verify that table is updated with proper version + assertEquals(Long.valueOf(1L), dynamoMapper.load(obj).getVersion()); + // Verify that in-memory copy is updated with proper version + assertEquals(Long.valueOf(1L), obj.getVersion()); + } + + @Test + public void testTransactionWriteForNonNullVersionAttributeSucceeds() { + RangeKeyClass obj = getUniqueRangeKeyObject(); + DynamoDBMapperTableModel model = getTable(obj); + DynamoDBMapperFieldModel val = model.field("version"); + assertEquals(true, val.versioned()); + assertEquals(null, obj.getVersion()); + dynamoMapper.save(obj); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addPut(obj); + dynamoMapper.transactionWrite(transactionWriteRequest); + // Verify that table is updated with proper version + assertEquals(Long.valueOf(2L), dynamoMapper.load(obj).getVersion()); + // Verify that in-memory copy is updated with proper version + assertEquals(Long.valueOf(2L), obj.getVersion()); + } + + @Test + public void testTransactionWriteForHigherVersionAttributeFails() { + RangeKeyClass obj = getUniqueRangeKeyObject(); + DynamoDBMapperTableModel model = getTable(obj); + DynamoDBMapperFieldModel val = model.field("version"); + assertEquals(true, val.versioned()); + dynamoMapper.save(obj); + assertEquals(Long.valueOf(1L), obj.getVersion()); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + obj.setVersion(3L); + transactionWriteRequest.addPut(obj); + try { + dynamoMapper.transactionWrite(transactionWriteRequest); + fail("Expected TransactionCanceledException but no exception thrown"); + } catch (TransactionCanceledException tce) { + // Expected + } + } + + @Test + public void testTransactionWriteForLowerVersionAttributeFails() { + RangeKeyClass obj = getUniqueRangeKeyObject(); + DynamoDBMapperTableModel model = getTable(obj); + DynamoDBMapperFieldModel val = model.field("version"); + assertEquals(true, val.versioned()); + dynamoMapper.save(obj); + assertEquals(Long.valueOf(1L), obj.getVersion()); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + obj.setVersion(0L); + transactionWriteRequest.addPut(obj); + try { + dynamoMapper.transactionWrite(transactionWriteRequest); + fail("Expected TransactionCanceledException but no exception thrown"); + } catch (TransactionCanceledException tce) { + // Expected + } + } + + @Test + public void testTransactionWriteForNullVersionAttributeFailsIfVersionIsPresentInRecord() { + RangeKeyClass obj = getUniqueRangeKeyObject(); + DynamoDBMapperTableModel model = getTable(obj); + DynamoDBMapperFieldModel val = model.field("version"); + assertEquals(true, val.versioned()); + dynamoMapper.save(obj); + assertEquals(Long.valueOf(1L), obj.getVersion()); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + obj.setVersion(null); + transactionWriteRequest.addPut(obj); + try { + dynamoMapper.transactionWrite(transactionWriteRequest); + fail("Expected TransactionCanceledException but no exception thrown"); + } catch (TransactionCanceledException tce) { + // Expected + } + } + + @Test + public void testMultiItemTransactionWriteWithVersionAttributeSucceeds() { + RangeKeyClass putObj = getUniqueRangeKeyObject(); + RangeKeyClass updateObj = getUniqueRangeKeyObject(); + RangeKeyClass deleteObj = getUniqueRangeKeyObject(); + dynamoMapper.save(putObj); + dynamoMapper.save(updateObj); + dynamoMapper.save(deleteObj); + assertEquals(Long.valueOf(1L), putObj.getVersion()); + assertEquals(Long.valueOf(1L), updateObj.getVersion()); + assertEquals(Long.valueOf(1L), deleteObj.getVersion()); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addPut(putObj); + transactionWriteRequest.addUpdate(updateObj); + transactionWriteRequest.addDelete(deleteObj); + dynamoMapper.transactionWrite(transactionWriteRequest); + // Verify that table is updated with proper version + assertEquals(Long.valueOf(2L), dynamoMapper.load(putObj).getVersion()); + assertEquals(Long.valueOf(2L), dynamoMapper.load(updateObj).getVersion()); + assertEquals(null, dynamoMapper.load(deleteObj)); + } + + @Test + public void testMultiItemTransactionWriteWithVersionAttributeAndConditionExpressionFails() { + RangeKeyClass putObj = getUniqueRangeKeyObject(); + RangeKeyClass updateObj = getUniqueRangeKeyObject(); + RangeKeyClass conditionCheckObj = getUniqueRangeKeyObject(); + RangeKeyClass deleteObj = getUniqueRangeKeyObject(); + dynamoMapper.save(putObj); + dynamoMapper.save(updateObj); + dynamoMapper.save(conditionCheckObj); + dynamoMapper.save(deleteObj); + assertEquals(Long.valueOf(1L), putObj.getVersion()); + assertEquals(Long.valueOf(1L), updateObj.getVersion()); + assertEquals(Long.valueOf(1L), conditionCheckObj.getVersion()); + assertEquals(Long.valueOf(1L), deleteObj.getVersion()); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + DynamoDBTransactionWriteExpression writeExpression = new DynamoDBTransactionWriteExpression(); + writeExpression.withConditionExpression("SomeConditionExpression"); + transactionWriteRequest.addPut(putObj, writeExpression); + transactionWriteRequest.addUpdate(updateObj, writeExpression); + transactionWriteRequest.addUpdate(conditionCheckObj, writeExpression); + transactionWriteRequest.addDelete(deleteObj, writeExpression); + try { + dynamoMapper.transactionWrite(transactionWriteRequest); + fail("Expected SdkClientException but no exception thrown"); + } catch (SdkClientException se) { + assertEquals(se.getMessage(), VERSION_CONDITION_CONFLICT_ERROR_MESSAGE); + } + } + + @Test + public void testTransactionWriteWithNullValuedMultiVersionAttributeItemSucceeds() { + MultiVersionRangeKeyClass obj = getMultiVersionRangeKeyObject(); + DynamoDBMapperTableModel model = getTable(obj); + DynamoDBMapperFieldModel val = model.field("version"); + assertEquals(true, val.versioned()); + assertEquals(null, obj.getVersion()); + assertEquals(null, obj.getVersion2()); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addPut(obj); + dynamoMapper.transactionWrite(transactionWriteRequest); + // Verify that table is updated with proper version + assertEquals(Long.valueOf(1L), dynamoMapper.load(obj).getVersion()); + assertEquals(Long.valueOf(1L), dynamoMapper.load(obj).getVersion2()); + // Verify that in-memory copy is updated with proper version + assertEquals(Long.valueOf(1L), obj.getVersion()); + assertEquals(Long.valueOf(1L), obj.getVersion2()); + } + + @Test + public void testTransactionWriteWithLowerValuedMultiVersionAttributeItemFails() { + MultiVersionRangeKeyClass obj = getMultiVersionRangeKeyObject(); + DynamoDBMapperTableModel model = getTable(obj); + DynamoDBMapperFieldModel val = model.field("version"); + assertEquals(true, val.versioned()); + assertEquals(null, obj.getVersion()); + assertEquals(null, obj.getVersion2()); + dynamoMapper.save(obj); + obj.setVersion2(0L); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addPut(obj); + try { + dynamoMapper.transactionWrite(transactionWriteRequest); + fail("Expected TransactionCanceledException but no exception thrown"); + } catch (TransactionCanceledException tce) { + // Expected + } + } + + @Test + public void testTransactionWriteWithNonNullValuedMultiVersionAttributeItemSucceeds() { + MultiVersionRangeKeyClass obj = getMultiVersionRangeKeyObject(); + DynamoDBMapperTableModel model = getTable(obj); + DynamoDBMapperFieldModel val = model.field("version"); + assertEquals(true, val.versioned()); + assertEquals(null, obj.getVersion()); + assertEquals(null, obj.getVersion2()); + dynamoMapper.save(obj); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addPut(obj); + dynamoMapper.transactionWrite(transactionWriteRequest); + // Verify that table is updated with proper version + assertEquals(Long.valueOf(2L), dynamoMapper.load(obj).getVersion()); + assertEquals(Long.valueOf(2L), dynamoMapper.load(obj).getVersion2()); + // Verify that in-memory copy is updated with proper version + assertEquals(Long.valueOf(2L), obj.getVersion()); + assertEquals(Long.valueOf(2L), obj.getVersion2()); + } + + @Test + public void testTransactionWriteWithMultiVersionAttributeItemAndConditionExpressionFails() { + MultiVersionRangeKeyClass obj = getMultiVersionRangeKeyObject(); + DynamoDBMapperTableModel model = getTable(obj); + DynamoDBMapperFieldModel val = model.field("version"); + assertEquals(true, val.versioned()); + assertEquals(null, obj.getVersion()); + assertEquals(null, obj.getVersion2()); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + DynamoDBTransactionWriteExpression writeExpression = new DynamoDBTransactionWriteExpression(); + writeExpression.withConditionExpression("SomeConditionExpression"); + transactionWriteRequest.addPut(obj, writeExpression); + try { + dynamoMapper.transactionWrite(transactionWriteRequest); + fail("Expected SdkClientException but no exception thrown"); + } catch (SdkClientException se) { + assertEquals(se.getMessage(), VERSION_CONDITION_CONFLICT_ERROR_MESSAGE); + } + } + + @Test + public void testTransactionWriteWithIdempotencyTokenSucceeds() { + StringAttributeClass obj = getStringAttributeClass(); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addPut(obj); + transactionWriteRequest.withIdempotencyToken(UUID.randomUUID().toString()); + + executeTransactWriteAndVerify(transactionWriteRequest, obj, obj); + } + + @Test + public void testTransactionWriteOnNonExistingTableReturnsResourceNotFoundException() { + final int numItems = 10; + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + for ( int i = 0; i < numItems; i++ ) { + NoSuchTableClass obj = new NoSuchTableClass(); + obj.setKey("someKey"); + transactionWriteRequest.addPut(obj); + } + + try { + dynamoMapper.transactionWrite(transactionWriteRequest); + fail("Expected ResourceNotFoundException but no exception thrown"); + } catch (ResourceNotFoundException e) { + // Pass + } + } + + @Test + public void testMixedOperationsOnSingleTableSucceeds() { + + StringAttributeClass objectToBeUpdated = getStringAttributeClass(); + StringAttributeClass objectToBeConditionChecked = getStringAttributeClass(); + StringAttributeClass objectToBeDeleted = getStringAttributeClass(); + + // Save it so that we can update it inside transaction + dynamoMapper.save(objectToBeUpdated); + // Save it so that we can conditionCheck it inside transaction + dynamoMapper.save(objectToBeConditionChecked); + // Save it so that we can delete it inside transaction + dynamoMapper.save(objectToBeDeleted); + + // Save the object + StringAttributeClass objectToBePut = getStringAttributeClass(); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addPut(objectToBePut); + // Update the object + String updatedStringAttribute = "updatedStringAttribute"; + objectToBeUpdated.setStringAttribute(updatedStringAttribute); + transactionWriteRequest.addUpdate(objectToBeUpdated); + // ConditionCheck the object + DynamoDBTransactionWriteExpression conditionCheckWriteExpression = + generateStringMatcherTransactWriteExpression(objectToBeConditionChecked.getStringAttribute(), + "not" + updatedStringAttribute); + transactionWriteRequest.addConditionCheck(objectToBeConditionChecked, conditionCheckWriteExpression); + // Delete the object + StringAttributeClass objectToBeDeletedKey = new StringAttributeClass(); + objectToBeDeletedKey.setKey(objectToBeDeleted.getKey()); + + transactionWriteRequest.addDelete(objectToBeDeletedKey); + + // Ensure transactWrite succeeds implying conditionCheck succeeds + dynamoMapper.transactionWrite(transactionWriteRequest); + + // Ensure toPut object is put + assertEquals(objectToBePut, dynamoMapper.load(objectToBePut)); + // Ensure toUpdate object is updated + assertEquals(updatedStringAttribute, dynamoMapper.load(objectToBeUpdated).getStringAttribute()); + // Ensure toDelete object is deleted + assertEquals(null, dynamoMapper.load(objectToBeDeleted)); + } + + @Test + public void testPutViaTransactionWriteWithConditionExpressionSucceeds() { + StringAttributeClass objectToBeSaved = getStringAttributeClass(); + dynamoMapper.save(objectToBeSaved); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + String oldStringValue = objectToBeSaved.getStringAttribute(); + String newStringValue = "saveStringValue"; + objectToBeSaved.setStringAttribute(newStringValue); + // Update the object with condition on same attribute that is being updated + DynamoDBTransactionWriteExpression saveItemExpression = + generateStringMatcherTransactWriteExpression(oldStringValue, "not" + oldStringValue); + transactionWriteRequest.addPut(objectToBeSaved, saveItemExpression); + + executeTransactWriteAndVerify(transactionWriteRequest, objectToBeSaved, objectToBeSaved); + } + + @Test + public void testDeleteViaTransactionWriteWithConditionExpressionSucceeds() { + StringAttributeClass objectToBeDeleted = getStringAttributeClass(); + dynamoMapper.save(objectToBeDeleted); + assertEquals(objectToBeDeleted, dynamoMapper.load(objectToBeDeleted)); + + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + // Update the object with condition on same attribute that is being updated + DynamoDBTransactionWriteExpression saveItemExpression = + generateStringMatcherTransactWriteExpression(objectToBeDeleted.getStringAttribute(), + "not" + objectToBeDeleted.getStringAttribute()); + transactionWriteRequest.addDelete(objectToBeDeleted, saveItemExpression); + + executeTransactWriteAndVerify(transactionWriteRequest, objectToBeDeleted, null); + } + + @Test + public void testUpdateItemWithConditionExpressionSucceeds() { + StringAttributeClass objectToBeUpdated = getStringAttributeClass(); + dynamoMapper.save(objectToBeUpdated); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + + String updatedStringAttribute = "updatedStringAttribute"; + String oldUpdateStringAttribute = objectToBeUpdated.getStringAttribute(); + + // Update the object with condition on same attribute that is being updated + objectToBeUpdated.setStringAttribute(updatedStringAttribute); + DynamoDBTransactionWriteExpression updateItemExpression = + generateStringMatcherTransactWriteExpression(oldUpdateStringAttribute, "not" + updatedStringAttribute); + transactionWriteRequest.addUpdate(objectToBeUpdated, updateItemExpression); + + executeTransactWriteAndVerify(transactionWriteRequest, objectToBeUpdated, objectToBeUpdated); + } + + @Test + public void testUpdateItemWithConditionExpressionFails() { + StringAttributeClass objectToBeUpdated = getStringAttributeClass(); + dynamoMapper.save(objectToBeUpdated); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + + String updatedStringAttribute = "updatedStringAttribute"; + String oldUpdateStringAttribute = objectToBeUpdated.getStringAttribute(); + + // Update the object with condition on same attribute that is being updated + objectToBeUpdated.setStringAttribute(updatedStringAttribute); + DynamoDBTransactionWriteExpression updateItemExpression = + generateStringMatcherTransactWriteExpression("not" + oldUpdateStringAttribute, + "notEither" + updatedStringAttribute); + transactionWriteRequest.addUpdate(objectToBeUpdated, updateItemExpression, ReturnValuesOnConditionCheckFailure.ALL_OLD); + + try { + dynamoMapper.transactionWrite(transactionWriteRequest); + fail("Expected TransactionCanceledException but no exception thrown"); + } catch (TransactionCanceledException tce) { + // Expected + } + } + + @Test + public void testUpdateItemWithAllNonKeyAttributesNullSucceeds() { + StringAttributeClass obj = getStringAttributeClass(); + dynamoMapper.save(obj); + assertEquals(obj, dynamoMapper.load(obj)); + + StringAttributeClass objToUpdate = new StringAttributeClass(); + objToUpdate.setKey(obj.getKey()); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + DynamoDBTransactionWriteExpression writeExpression = new DynamoDBTransactionWriteExpression(); + transactionWriteRequest.addUpdate(objToUpdate, writeExpression); + dynamoMapper.transactionWrite(transactionWriteRequest); + + StringAttributeClass updatedObject = dynamoMapper.load(obj); + assertEquals(null, updatedObject.getStringAttribute()); + assertEquals(null, updatedObject.getRenamedAttribute()); + } + + @Test + public void testUpdateItemWithMixedNullAndNonNullNonKeyAttributesSucceeds() { + StringAttributeClass obj = getStringAttributeClass(); + String updatedString = "updatedString"; + dynamoMapper.save(obj); + assertEquals(obj, dynamoMapper.load(obj)); + + StringAttributeClass objToUpdate = new StringAttributeClass(); + objToUpdate.setKey(obj.getKey()); + objToUpdate.setStringAttribute(updatedString); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + DynamoDBTransactionWriteExpression writeExpression = new DynamoDBTransactionWriteExpression(); + transactionWriteRequest.addUpdate(objToUpdate, writeExpression); + executeTransactWriteAndVerify(transactionWriteRequest, objToUpdate, objToUpdate); + } + + @Test + public void testUpdateItemWithMultipleMixedNullAndNonNullNonKeyAttributesSucceeds() { + TestItem obj = getTestItemObject(); + String updatedString = "updatedString"; + Set updatedStringSet = new HashSet(); + updatedStringSet.add("UpdatedStringSet"); + dynamoMapper.save(obj); + assertEquals(obj, dynamoMapper.load(obj)); + + TestItem objToUpdate = new TestItem(); + objToUpdate.setHashKey(obj.getHashKey()); + objToUpdate.setRangeKey(obj.getRangeKey()); + objToUpdate.setStringAttribute(updatedString); + objToUpdate.setStringSetAttribute(updatedStringSet); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + DynamoDBTransactionWriteExpression writeExpression = new DynamoDBTransactionWriteExpression(); + transactionWriteRequest.addUpdate(objToUpdate, writeExpression); + executeTransactWriteAndVerify(transactionWriteRequest, objToUpdate, objToUpdate); + } + + @Test + public void testUpdateItemWithTransformerSucceeds() { + DynamoDBMapper mapperWithTransformer = new DynamoDBMapper(dynamoDB, null, + new AttributeAdder(ADDITIONAL_ATTRIBUTE_NAME, ATTRIBUTE_ADDER_START)); + TestItem obj = getTestItemObject(); + String updatedString = "updatedString"; + Set updatedStringSet = new HashSet(); + updatedStringSet.add("UpdatedStringSet"); + mapperWithTransformer.save(obj); + assertEquals(obj, mapperWithTransformer.load(obj)); + + // Retrieve the item directly to verify signature field exists + Map key = new HashMap(); + key.put("hashKey", AttributeValue.builder().s(obj.getHashKey()).build()); + key.put("rangeKey", AttributeValue.builder().n(String.valueOf(obj.getRangeKey())).build()); + GetItemResponse item = dynamoDB.getItem(GetItemRequest.builder().tableName(TestItem.TABLE_NAME).key(key).build()); + assertTrue(item.item().containsKey(ADDITIONAL_ATTRIBUTE_NAME)); + assertEquals(ATTRIBUTE_ADDER_START, Integer.parseInt(item.item().get(ADDITIONAL_ATTRIBUTE_NAME).n())); + + TestItem objToUpdate = new TestItem(); + objToUpdate.setHashKey(obj.getHashKey()); + objToUpdate.setRangeKey(obj.getRangeKey()); + objToUpdate.setStringAttribute(updatedString); + objToUpdate.setStringSetAttribute(updatedStringSet); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + DynamoDBTransactionWriteExpression writeExpression = new DynamoDBTransactionWriteExpression(); + transactionWriteRequest.addUpdate(objToUpdate, writeExpression); + mapperWithTransformer.transactionWrite(transactionWriteRequest); + assertEquals(objToUpdate, mapperWithTransformer.load(objToUpdate)); + + // Retrieve the item directly to verify signature field exists and is different from prior signature + key.put("hashKey", AttributeValue.builder().s(objToUpdate.getHashKey()).build()); + key.put("rangeKey", AttributeValue.builder().n(String.valueOf(objToUpdate.getRangeKey())).build()); + GetItemResponse updatedItem = dynamoDB.getItem(GetItemRequest.builder().tableName(TestItem.TABLE_NAME).key(key).build()); + assertTrue(updatedItem.item().containsKey(ADDITIONAL_ATTRIBUTE_NAME)); + assertEquals(ATTRIBUTE_ADDER_START + 1, Integer.parseInt(updatedItem.item().get(ADDITIONAL_ATTRIBUTE_NAME).n())); + } + + @Test + public void testUpdateItemWithTransformerNullAttributesSucceeds() { + // Create object with additional attribute + DynamoDBMapper mapperWithTransformer = new DynamoDBMapper(dynamoDB, null, + new AttributeAdder(ADDITIONAL_ATTRIBUTE_NAME, ATTRIBUTE_ADDER_START)); + TestItem obj = getTestItemObject(); + String updatedString = "updatedString"; + Set updatedStringSet = new HashSet(); + updatedStringSet.add("UpdatedStringSet"); + mapperWithTransformer.save(obj); + assertEquals(obj, mapperWithTransformer.load(obj)); + + // Retrieve the item directly to verify additional field exists + Map key = new HashMap(); + key.put("hashKey", AttributeValue.builder().s(obj.getHashKey()).build()); + key.put("rangeKey", AttributeValue.builder().n(String.valueOf(obj.getRangeKey())).build()); + GetItemResponse item = dynamoDB.getItem(GetItemRequest.builder().tableName(TestItem.TABLE_NAME).key(key).build()); + assertTrue(item.item().containsKey(ADDITIONAL_ATTRIBUTE_NAME)); + assertEquals(ATTRIBUTE_ADDER_START, Integer.parseInt(item.item().get(ADDITIONAL_ATTRIBUTE_NAME).n())); + + // Update object with NullAttributeAdder (to verify removal of null non-modeled key) + DynamoDBMapper mapperWithNullTransformer = new DynamoDBMapper(dynamoDB, null, new NullAttributeAdder(ADDITIONAL_ATTRIBUTE_NAME)); + + TestItem objToUpdate = new TestItem(); + objToUpdate.setHashKey(obj.getHashKey()); + objToUpdate.setRangeKey(obj.getRangeKey()); + objToUpdate.setStringAttribute(updatedString); + objToUpdate.setStringSetAttribute(updatedStringSet); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + DynamoDBTransactionWriteExpression writeExpression = new DynamoDBTransactionWriteExpression(); + transactionWriteRequest.addUpdate(objToUpdate, writeExpression); + mapperWithNullTransformer.transactionWrite(transactionWriteRequest); + assertEquals(objToUpdate, mapperWithNullTransformer.load(objToUpdate)); + + // Retrieve the item directly to verify additional field doesn't exit + key.put("hashKey", AttributeValue.builder().s(objToUpdate.getHashKey()).build()); + key.put("rangeKey", AttributeValue.builder().n(String.valueOf(objToUpdate.getRangeKey())).build()); + GetItemResponse updatedItem = dynamoDB.getItem(GetItemRequest.builder().tableName(TestItem.TABLE_NAME).key(key).build()); + assertFalse(updatedItem.item().containsKey(ADDITIONAL_ATTRIBUTE_NAME)); + } + + @Test + public void testPutItemWithTransformerSucceeds() { + DynamoDBMapper mapperWithTransformer = new DynamoDBMapper(dynamoDB, null, + new AttributeAdder(ADDITIONAL_ATTRIBUTE_NAME, ATTRIBUTE_ADDER_START)); + TestItem obj = getTestItemObject(); + + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addPut(obj); + mapperWithTransformer.transactionWrite(transactionWriteRequest); + assertEquals(obj, mapperWithTransformer.load(obj)); + + // Retrieve the item directly to verify signature field exists + Map key = new HashMap(); + key.put("hashKey", AttributeValue.builder().s(obj.getHashKey()).build()); + key.put("rangeKey", AttributeValue.builder().n(String.valueOf(obj.getRangeKey())).build()); + GetItemResponse item = dynamoDB.getItem(GetItemRequest.builder().tableName(TestItem.TABLE_NAME).key(key).build()); + assertTrue(item.item().containsKey(ADDITIONAL_ATTRIBUTE_NAME)); + assertEquals(ATTRIBUTE_ADDER_START, Integer.parseInt(item.item().get(ADDITIONAL_ATTRIBUTE_NAME).n())); + } + + @Test + public void testPutViaTransactionWriteWithADocumentSucceeds() { + final ChildClass child1 = new ChildClass(); + child1.setBool(true); + + final ChildClass child2 = new ChildClass(); + child2.setBool(true); + + final ChildClass parent = new ChildClass(); + parent.setFirstChild(child1); + parent.setOtherChildren(Arrays.asList(child1, child2)); + parent.setNamedChildren(new HashMap() {{ + put("one", child1); + put("two", child2); + }}); + + TestDocClass obj = new TestDocClass(); + obj.setId("test"); + obj.setListOfMaps(Arrays.>asList( + new HashMap() {{ + put("parent", parent); + }}, + new HashMap() {{ + put("parent", parent); + }}, + null + )); + obj.setMapOfLists(new HashMap>() {{ + put("parent", Arrays.asList(child1, child2)); + put("child2", Collections.emptyList()); + put("child1", null); + }}); + + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addPut(obj); + dynamoMapper.transactionWrite(transactionWriteRequest); + + TransactionLoadRequest transactionLoadRequest = new TransactionLoadRequest(); + transactionLoadRequest.addLoad(obj); + List responseObjList = transactionLoadObjects(transactionLoadRequest); + assertTrue(responseObjList.size() == 1); + assertEquals(obj, responseObjList.get(0)); + } + + private void executeTransactWriteAndVerify(TransactionWriteRequest transactionWriteRequest, + Object requestKey, Object responseObject) { + dynamoMapper.transactionWrite(transactionWriteRequest); + assertEquals(responseObject, dynamoMapper.load(requestKey)); + } + + private DynamoDBTransactionWriteExpression generateStringMatcherTransactWriteExpression(String matchingString1, + String matchingString2) { + DynamoDBTransactionWriteExpression writeExpression = new DynamoDBTransactionWriteExpression(); + Map attributeNameMap = new HashMap(); + attributeNameMap.put("#sA", "stringAttribute"); + writeExpression.withConditionExpression("(#sA IN (:attr1, :attr2))"); + Map attributeValueMap = new HashMap(); + attributeValueMap.put(":attr1", AttributeValue.builder().s(matchingString1).build()); + attributeValueMap.put(":attr2", AttributeValue.builder().s(matchingString2).build()); + writeExpression.withExpressionAttributeValues(attributeValueMap); + writeExpression.withExpressionAttributeNames(attributeNameMap); + return writeExpression; + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/TransactionsUnitTestBase.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/TransactionsUnitTestBase.java new file mode 100644 index 000000000000..20dee6513020 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/TransactionsUnitTestBase.java @@ -0,0 +1,81 @@ +/* + * 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.mapper.dynamodb; +import software.amazon.awssdk.mapper.dynamodb.internal.DynamoDBMapperModelFactory; + +import software.amazon.awssdk.mapper.dynamodb.test.util.DynamoDBUnitTestBase; +import software.amazon.awssdk.services.dynamodb.model.CancellationReason; +import software.amazon.awssdk.services.dynamodb.model.TransactionCanceledException; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.junit.Assert.fail; + +public class TransactionsUnitTestBase extends DynamoDBUnitTestBase { + + protected static DynamoDBMapper dynamoMapper; + protected static final DynamoDBMapperModelFactory factory = StandardModelFactories.of(S3Link.Factory.of(null)); + protected static final DynamoDBMapperModelFactory.TableFactory models = factory.getTableFactory(DynamoDBMapperConfig.DEFAULT); + + @SuppressWarnings("unchecked") + protected static DynamoDBMapperTableModel getTable(T object) { + return models.getTable((Class)object.getClass()); + } + + /** + * Use this method to transactionLoad objects for verification after executing transactionWrite. + */ + protected List transactionLoadObjects(TransactionLoadRequest transactionLoadRequest) { + return transactionLoadObjects(transactionLoadRequest, DynamoDBMapperConfig.DEFAULT); + } + + /** + * Use this method to transactionLoad objects for verification after executing transactionWrite. + */ + protected List transactionLoadObjects(TransactionLoadRequest transactionLoadRequest, DynamoDBMapperConfig config) { + // transactionLoad can sometimes fail due to conflict with a recently submitted transactionWrite which is not yet complete, so we re-try for + // some time when that type of failure is encountered + long endTime = System.currentTimeMillis() + 30 * 1000; + List actualResponseObjects = null; + while (System.currentTimeMillis() < endTime) { + try { + actualResponseObjects = dynamoMapper.transactionLoad(transactionLoadRequest, config); + break; + } catch (TransactionCanceledException tce) { + List cancellationReasons = tce.cancellationReasons(); + Set uniqueCancellationReasonCodes = new HashSet(); + for (CancellationReason cancellationReason: cancellationReasons) { + uniqueCancellationReasonCodes.add(cancellationReason.code()); + } + if (uniqueCancellationReasonCodes.size() != 1 || !"TransactionConflict".equals(cancellationReasons.get(0).code())) { + fail("transactionLoad failed with TransactionCanceledException having non-TransactionConflict cancellation reason(s): " + tce); + } + // Sleep for some time before re-trying transactionLoad + try { + Thread.sleep(1000); + } catch (InterruptedException ie) { + fail("interrupted while waiting to re-try transactionLoad: " + ie); + } + } + } + if (actualResponseObjects == null) { + fail("Timed out while executing transactionLoad due to conflict with ongoing transactionWrite"); + } + return actualResponseObjects; + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/UnmarshallerTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/UnmarshallerTest.java new file mode 100644 index 000000000000..46427b5dd675 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/UnmarshallerTest.java @@ -0,0 +1,33 @@ +/* + * 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.mapper.dynamodb; + +import java.lang.reflect.Method; + +import com.amazonaws.auth.AWSCredentialsProvider; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +public class UnmarshallerTest extends StandardModelFactoriesV2UnconvertTest { + + private static final ItemConverter CONVERTER = CONFIG.getConversionSchema().getConverter( + new ConversionSchema.Dependencies().with(S3ClientCache.class, new S3ClientCache((AWSCredentialsProvider)null))); + + @Override + protected Object unconvert(Class clazz, Method getter, Method setter, AttributeValue value) { + return CONVERTER.unconvert(getter, setter, value); + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/V1MarshallerTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/V1MarshallerTest.java new file mode 100644 index 000000000000..f5cd55378176 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/V1MarshallerTest.java @@ -0,0 +1,32 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.mapper.dynamodb; + +import java.lang.reflect.Method; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +public class V1MarshallerTest extends StandardModelFactoriesV1Test { + + private static final ItemConverter CONVERTER = CONFIG.getConversionSchema().getConverter( + new ConversionSchema.Dependencies()); + + @Override + protected AttributeValue convert(Class clazz, Method getter, Object value) { + return CONVERTER.convert(getter, value); + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/V2CompatMarshallerTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/V2CompatMarshallerTest.java new file mode 100644 index 000000000000..976a1b2b2474 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/V2CompatMarshallerTest.java @@ -0,0 +1,32 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.mapper.dynamodb; + +import java.lang.reflect.Method; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +public class V2CompatMarshallerTest extends StandardModelFactoriesV2CompatibleTest { + + private static final ItemConverter CONVERTER = CONFIG.getConversionSchema().getConverter( + new ConversionSchema.Dependencies()); + + @Override + protected AttributeValue convert(Class clazz, Method getter, Object value) { + return CONVERTER.convert(getter, value); + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/V2MarshallerTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/V2MarshallerTest.java new file mode 100644 index 000000000000..f45e86c3ded9 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/V2MarshallerTest.java @@ -0,0 +1,32 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.mapper.dynamodb; + +import java.lang.reflect.Method; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +public class V2MarshallerTest extends StandardModelFactoriesV2Test { + + private static final ItemConverter CONVERTER = CONFIG.getConversionSchema().getConverter( + new ConversionSchema.Dependencies()); + + @Override + protected AttributeValue convert(Class clazz, Method getter, Object value) { + return CONVERTER.convert(getter, value); + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/VersionAttributeConditionExpressionGeneratorUnitTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/VersionAttributeConditionExpressionGeneratorUnitTest.java new file mode 100644 index 000000000000..284d445dc86a --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/VersionAttributeConditionExpressionGeneratorUnitTest.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.mapper.dynamodb; +import software.amazon.awssdk.mapper.dynamodb.internal.DynamoDBMapperModelFactory; +import software.amazon.awssdk.mapper.dynamodb.internal.VersionAttributeConditionExpressionGenerator; + +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.getMultiVersionRangeKeyObject; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.getUniqueRangeKeyObject; +import static org.junit.Assert.assertEquals; + +import java.util.HashMap; +import java.util.Map; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.mapper.dynamodb.pojos.MultiVersionRangeKeyClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.RangeKeyClass; +import org.junit.Test; + +public class VersionAttributeConditionExpressionGeneratorUnitTest { + + private static final DynamoDBMapperModelFactory factory = StandardModelFactories.of(S3Link.Factory.of(null)); + private static final DynamoDBMapperModelFactory.TableFactory models = + factory.getTableFactory(DynamoDBMapperConfig.DEFAULT); + + @SuppressWarnings("unchecked") + private static DynamoDBMapperTableModel getTable(T object) { + return models.getTable((Class)object.getClass()); + } + + @Test + public void testExpressionGeneratedForOneNullValuedVersion() { + VersionAttributeConditionExpressionGenerator versionAttributeConditionExpressionGenerator = + new VersionAttributeConditionExpressionGenerator(); + Object obj = getUniqueRangeKeyObject(); + final DynamoDBMapperTableModel model = getTable((Object)obj); + DynamoDBMapperFieldModel versionedField = model.field("version"); + Object fieldValue = versionedField.get(obj); + versionAttributeConditionExpressionGenerator.appendVersionAttributeToConditionExpression(versionedField, + fieldValue); + String expectedConditionExpression = "attribute_not_exists(#versionAttributeName1)"; + Map expectedExpressionAttributeNames = new HashMap(); + expectedExpressionAttributeNames.put("#versionAttributeName1", "version"); + assertVersionAttributeConditionExpression(versionAttributeConditionExpressionGenerator + .getVersionAttributeConditionExpression(), + expectedConditionExpression, + expectedExpressionAttributeNames, + null /* expectedExpressionAttributeValues */); + } + + @Test + public void testExpressionGeneratedForOneNonNullValuedVersion() { + VersionAttributeConditionExpressionGenerator versionAttributeConditionExpressionGenerator = + new VersionAttributeConditionExpressionGenerator(); + RangeKeyClass obj = getUniqueRangeKeyObject(); + final DynamoDBMapperTableModel model = getTable((Object)obj); + obj.setVersion(1L); + DynamoDBMapperFieldModel versionedField = model.field("version"); + Object fieldValue = versionedField.get(obj); + versionAttributeConditionExpressionGenerator.appendVersionAttributeToConditionExpression(versionedField, + fieldValue); + String expectedConditionExpression = "attribute_exists(#versionAttributeName1) " + + "AND #versionAttributeName1 = :versionAttributeValue1"; + Map expectedExpressionAttributeNames = new HashMap(); + expectedExpressionAttributeNames.put("#versionAttributeName1", "version"); + Map expectedExpressionAttributeValues = new HashMap(); + expectedExpressionAttributeValues.put(":versionAttributeValue1", versionedField.convert(fieldValue)); + assertVersionAttributeConditionExpression(versionAttributeConditionExpressionGenerator + .getVersionAttributeConditionExpression(), + expectedConditionExpression, + expectedExpressionAttributeNames, + expectedExpressionAttributeValues); + } + + @Test + public void testExpressionGeneratedForMultipleNullValuedVersions() { + VersionAttributeConditionExpressionGenerator versionAttributeConditionExpressionGenerator = + new VersionAttributeConditionExpressionGenerator(); + MultiVersionRangeKeyClass obj = getMultiVersionRangeKeyObject(); + final DynamoDBMapperTableModel model = getTable((Object)obj); + DynamoDBMapperFieldModel versionedField1 = model.field("version"); + Object fieldValue1 = versionedField1.get(obj); + versionAttributeConditionExpressionGenerator.appendVersionAttributeToConditionExpression(versionedField1, + fieldValue1); + DynamoDBMapperFieldModel versionedField2 = model.field("version2"); + Object fieldValue2 = versionedField2.get(obj); + versionAttributeConditionExpressionGenerator.appendVersionAttributeToConditionExpression(versionedField2, + fieldValue2); + Map expectedExpressionAttributeNames = new HashMap(); + expectedExpressionAttributeNames.put("#versionAttributeName1", "version"); + expectedExpressionAttributeNames.put("#versionAttributeName2", "version2"); + String expectedConditionExpression = "(attribute_not_exists(#versionAttributeName1)) " + + "AND (attribute_not_exists(#versionAttributeName2))"; + assertVersionAttributeConditionExpression(versionAttributeConditionExpressionGenerator + .getVersionAttributeConditionExpression(), + expectedConditionExpression, + expectedExpressionAttributeNames, + null /* expectedExpressionAttributeValues */); + } + + @Test + public void testExpressionGeneratedForMultipleNonNullValuedVersion() { + VersionAttributeConditionExpressionGenerator versionAttributeConditionExpressionGenerator = + new VersionAttributeConditionExpressionGenerator(); + MultiVersionRangeKeyClass obj = getMultiVersionRangeKeyObject(); + final DynamoDBMapperTableModel model = getTable((Object)obj); + obj.setVersion(1L); + obj.setVersion2(1L); + DynamoDBMapperFieldModel versionedField1 = model.field("version"); + Object fieldValue1 = versionedField1.get(obj); + versionAttributeConditionExpressionGenerator.appendVersionAttributeToConditionExpression(versionedField1, + fieldValue1); + DynamoDBMapperFieldModel versionedField2 = model.field("version2"); + Object fieldValue2 = versionedField2.get(obj); + versionAttributeConditionExpressionGenerator.appendVersionAttributeToConditionExpression(versionedField2, + fieldValue2); + Map expectedExpressionAttributeNames = new HashMap(); + expectedExpressionAttributeNames.put("#versionAttributeName1", "version"); + expectedExpressionAttributeNames.put("#versionAttributeName2", "version2"); + Map expectedExpressionAttributeValues = new HashMap(); + expectedExpressionAttributeValues.put(":versionAttributeValue1", versionedField1.convert(fieldValue1)); + expectedExpressionAttributeValues.put(":versionAttributeValue2", versionedField1.convert(fieldValue2)); + String expectedConditionExpression = + "(attribute_exists(#versionAttributeName1) AND #versionAttributeName1 = :versionAttributeValue1) " + + "AND (attribute_exists(#versionAttributeName2) AND #versionAttributeName2 = :versionAttributeValue2)"; + assertVersionAttributeConditionExpression(versionAttributeConditionExpressionGenerator + .getVersionAttributeConditionExpression(), + expectedConditionExpression, + expectedExpressionAttributeNames, + expectedExpressionAttributeValues); + } + + private void assertVersionAttributeConditionExpression( + DynamoDBTransactionWriteExpression actualVersionAttributeConditionExpression, + String expectedConditionExpression, + Map expectedExpressionAttributeNames, + Map expectedExpressionAttributeValues) { + assertEquals(expectedConditionExpression, actualVersionAttributeConditionExpression.getConditionExpression()); + assertEquals(expectedExpressionAttributeNames, + actualVersionAttributeConditionExpression.getExpressionAttributeNames()); + assertEquals(expectedExpressionAttributeValues, + actualVersionAttributeConditionExpression.getExpressionAttributeValues()); + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/AbstractKeyAndValIntegrationTestCase.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/AbstractKeyAndValIntegrationTestCase.java new file mode 100644 index 000000000000..fc28b604beca --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/AbstractKeyAndValIntegrationTestCase.java @@ -0,0 +1,124 @@ +/* + * 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.mapper.dynamodb.mapper; + +import static software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.ConsistentReads.CONSISTENT; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperIntegrationTestBase; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.SaveBehavior; +import software.amazon.awssdk.mapper.dynamodb.pojos.KeyAndVal; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.After; +import org.junit.Before; + +/** + * Tests updating component attribute fields correctly. + */ +public abstract class AbstractKeyAndValIntegrationTestCase extends DynamoDBMapperIntegrationTestBase { + + /** + * The DynamoDBMapper instance. + */ + protected DynamoDBMapper util; + + /** + * Sets up the test case. + */ + protected final void setUpTest(final SaveBehavior saveBehavior) { + this.util = new DynamoDBMapper(dynamo, new DynamoDBMapperConfig.Builder().withSaveBehavior(saveBehavior) + .withConsistentReads(CONSISTENT) + .build()); + } + + /** + * Sets up the test case. + */ + @Before + public void setUpTest() { + setUpTest(DynamoDBMapperConfig.DEFAULT.getSaveBehavior()); + } + + /** + * Tears down the test case. + */ + @After + public void tearDownTest() { + this.util = null; + } + + /** + * Assert that the object updated appropriately. + * @param changeExpected True if a change is expected. + * @param objects The objects. + */ + protected final void assertBeforeAndAfterChange(final boolean changeExpected, final List> objects) { + final List befores = new ArrayList(objects.size()); + for (final KeyAndVal object : objects) { + befores.add(object.getVal()); + } + this.util.batchSave(objects); + for (int i = 0, its = objects.size(); i < its; i++) { + assertBeforeAndAfterChange(changeExpected, befores.get(i), objects.get(i).getVal()); + } + } + + /** + * Assert that the object updated appropriately. + * @param changeExpected True if a change is expected. + * @param object The object. + * @return The value if more assertions are required. + */ + protected final V assertBeforeAndAfterChange(final Boolean changeExpected, final KeyAndVal object) { + final V before = object.getVal(); + this.util.save(object); + final V after = object.getVal(); + if (changeExpected != null) { + assertBeforeAndAfterChange(changeExpected, before, after); + } + final KeyAndVal reload = this.util.load(object.getClass(), object.getKey()); + assertNotNull(reload); + if (changeExpected != null) { + assertBeforeAndAfterChange(false, after, reload.getVal()); + assertBeforeAndAfterChange(changeExpected, before, reload.getVal()); + } + return reload.getVal(); + } + + /** + * Assert that the object updated appropriately. + * @param changeExpected True if a change is expected. + * @param before The before value. + * @param after The after value. + */ + protected final void assertBeforeAndAfterChange(final boolean changeExpected, final V before, final V after) { + if (!changeExpected) { + assertEquals(String.format("Expected before[%s] and after[%s] to be equal", before, after), before, after); + } else if (before == null) { + assertNotNull(String.format("Expected after[%s] to not be null", after), after); + } else { + assertFalse(String.format("Expected before[%s] and after[%s] to not be equal", before, after), before.equals(after)); + } + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/AutoGeneratedKeysIntegrationTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/AutoGeneratedKeysIntegrationTest.java new file mode 100644 index 000000000000..8885de5f678d --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/AutoGeneratedKeysIntegrationTest.java @@ -0,0 +1,834 @@ +/* + * 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.mapper.dynamodb.mapper; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +import java.util.Collections; +import java.util.UUID; + +import software.amazon.awssdk.mapper.dynamodb.pojos.HashKeyAutoGenerated; +import software.amazon.awssdk.mapper.dynamodb.pojos.HashKeyRangeKeyBothAutoGenerated; +import software.amazon.awssdk.mapper.dynamodb.pojos.PrimaryKeysNotAutogenerated_IndexKeysAutogenerated; +import org.junit.BeforeClass; +import org.junit.Test; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperIntegrationTestBase; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAutoGeneratedKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBIndexHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBIndexRangeKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMappingException; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBRangeKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBSaveExpression; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; +import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException; +import software.amazon.awssdk.services.dynamodb.model.ConditionalOperator; +import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest; +import software.amazon.awssdk.services.dynamodb.model.ExpectedAttributeValue; +import software.amazon.awssdk.services.dynamodb.model.GlobalSecondaryIndex; +import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement; +import software.amazon.awssdk.services.dynamodb.model.KeyType; +import software.amazon.awssdk.services.dynamodb.model.Projection; +import software.amazon.awssdk.services.dynamodb.model.ProjectionType; +import software.amazon.awssdk.services.dynamodb.model.ProvisionedThroughput; +import software.amazon.awssdk.services.dynamodb.model.ResourceInUseException; +import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType; +import java.util.HashMap; +import java.util.Map; + +/** + * Tests using auto-generated keys for range keys, hash keys, or both. + */ +public class AutoGeneratedKeysIntegrationTest extends DynamoDBMapperIntegrationTestBase { + + private static final String TABLE_NAME = "aws-java-sdk-string-range"; + + private static final String GSI_NAME = "gsi-with-autogenerated-keys"; + private static final String GSI_HASH_KEY = "gis-hash-key"; + private static final String GSI_RANGE_KEY = "gis-range-key"; + + @BeforeClass + public static void setUp() throws Exception { + DynamoDBMapperIntegrationTestBase.setUp(); + + String keyName = DynamoDBMapperIntegrationTestBase.KEY_NAME; + String rangeKeyAttributeName = "rangeKey"; + + CreateTableRequest createTableRequest = CreateTableRequest.builder() + .tableName(TABLE_NAME) + .keySchema( + KeySchemaElement.builder().attributeName(keyName).keyType(KeyType.HASH).build(), + KeySchemaElement.builder().attributeName(rangeKeyAttributeName).keyType(KeyType.RANGE).build()) + .globalSecondaryIndexes(GlobalSecondaryIndex.builder() + .indexName(GSI_NAME) + .keySchema( + KeySchemaElement.builder().attributeName(GSI_HASH_KEY).keyType(KeyType.HASH).build(), + KeySchemaElement.builder().attributeName(GSI_RANGE_KEY).keyType(KeyType.RANGE).build()) + .projection(Projection.builder() + .projectionType(ProjectionType.ALL).build()) + .provisionedThroughput(ProvisionedThroughput.builder().readCapacityUnits(3L).writeCapacityUnits(3L).build()).build()) + .attributeDefinitions( + AttributeDefinition.builder().attributeName(keyName).attributeType(ScalarAttributeType.S).build(), + AttributeDefinition.builder().attributeName(rangeKeyAttributeName).attributeType(ScalarAttributeType.S).build(), + AttributeDefinition.builder().attributeName(GSI_HASH_KEY).attributeType(ScalarAttributeType.S).build(), + AttributeDefinition.builder().attributeName(GSI_RANGE_KEY).attributeType(ScalarAttributeType.S).build()) + .provisionedThroughput(ProvisionedThroughput.builder().readCapacityUnits(10L).writeCapacityUnits(5L).build()) + .build(); + + try { + dynamo.createTable(createTableRequest); + dynamo.waiter().waitUntilTableExists(b -> b.tableName(TABLE_NAME)); + } catch (ResourceInUseException e) { + // Table already exists. + } + } + + @Test + public void testHashKeyRangeKeyBothAutogenerated() { + DynamoDBMapper mapper = new DynamoDBMapper(dynamo); + HashKeyRangeKeyBothAutoGenerated obj = new HashKeyRangeKeyBothAutoGenerated(); + obj.setOtherAttribute("blah"); + + assertNull(obj.getKey()); + assertNull(obj.getRangeKey()); + mapper.save(obj); + assertNotNull(obj.getKey()); + assertNotNull(obj.getRangeKey()); + + HashKeyRangeKeyBothAutoGenerated other = mapper.load(HashKeyRangeKeyBothAutoGenerated.class, obj.getKey(), + obj.getRangeKey()); + assertEquals(other, obj); + } + + @Test + public void testHashKeyRangeKeyBothAutogeneratedBatchWrite() { + DynamoDBMapper mapper = new DynamoDBMapper(dynamo); + HashKeyRangeKeyBothAutoGenerated obj = new HashKeyRangeKeyBothAutoGenerated(); + obj.setOtherAttribute("blah"); + HashKeyRangeKeyBothAutoGenerated obj2 = new HashKeyRangeKeyBothAutoGenerated(); + obj2.setOtherAttribute("blah"); + + assertNull(obj.getKey()); + assertNull(obj.getRangeKey()); + assertNull(obj2.getKey()); + assertNull(obj2.getRangeKey()); + mapper.batchSave(obj, obj2); + assertNotNull(obj.getKey()); + assertNotNull(obj.getRangeKey()); + assertNotNull(obj2.getKey()); + assertNotNull(obj2.getRangeKey()); + + assertEquals(mapper.load(HashKeyRangeKeyBothAutoGenerated.class, obj.getKey(), + obj.getRangeKey()), obj); + assertEquals(mapper.load(HashKeyRangeKeyBothAutoGenerated.class, obj2.getKey(), + obj2.getRangeKey()), obj2); + } + + /** + * Tests providing additional expected conditions when saving item with + * auto-generated keys. + */ + @Test + public void testAutogeneratedKeyWithUserProvidedExpectedConditions() { + DynamoDBMapper mapper = new DynamoDBMapper(dynamo); + HashKeyRangeKeyBothAutoGenerated obj = new HashKeyRangeKeyBothAutoGenerated(); + obj.setOtherAttribute("blah"); + + assertNull(obj.getKey()); + assertNull(obj.getRangeKey()); + + // Add additional expected conditions via DynamoDBSaveExpression. + // Expected conditions joined by AND are compatible with the conditions + // for auto-generated keys. + DynamoDBSaveExpression saveExpression = new DynamoDBSaveExpression(); + saveExpression + .withExpected(Collections.singletonMap( + "otherAttribute", ExpectedAttributeValue.builder().exists(false).build())) + .withConditionalOperator(ConditionalOperator.AND); + // The save should succeed since the user provided conditions are joined by AND. + mapper.save(obj, saveExpression); + assertNotNull(obj.getKey()); + assertNotNull(obj.getRangeKey()); + + HashKeyRangeKeyBothAutoGenerated other = mapper.load(HashKeyRangeKeyBothAutoGenerated.class, obj.getKey(), + obj.getRangeKey()); + assertEquals(other, obj); + + // Change the conditional operator to OR. + // IllegalArgumentException is expected since the additional expected + // conditions cannot be joined with the conditions for auto-generated + // keys. + saveExpression.setConditionalOperator(ConditionalOperator.OR); + try { + mapper.save(new HashKeyRangeKeyBothAutoGenerated(), saveExpression); + } catch (IllegalArgumentException expected) {} + + // User-provided OR conditions should work if they completely override the generated conditions. + Map overrideConditions = new HashMap(); + overrideConditions.put("otherAttribute", ExpectedAttributeValue.builder().exists(false).build()); + overrideConditions.put("key", ExpectedAttributeValue.builder().exists(false).build()); + overrideConditions.put("rangeKey", ExpectedAttributeValue.builder().exists(false).build()); + saveExpression + .withExpected(overrideConditions) + .withConditionalOperator(ConditionalOperator.OR); + mapper.save(new HashKeyRangeKeyBothAutoGenerated(), saveExpression); + + Map nonExistentConditions = new HashMap(); + nonExistentConditions.put("otherAttribute", ExpectedAttributeValue.builder().value(AttributeValue.builder().s("non-existent-value").build()).build()); + nonExistentConditions.put("key", ExpectedAttributeValue.builder().value(AttributeValue.builder().s("non-existent-value").build()).build()); + nonExistentConditions.put("rangeKey", ExpectedAttributeValue.builder().value(AttributeValue.builder().s("non-existent-value").build()).build()); + saveExpression + .withExpected(nonExistentConditions) + .withConditionalOperator(ConditionalOperator.OR); + try { + mapper.save(new HashKeyRangeKeyBothAutoGenerated(), saveExpression); + } catch (ConditionalCheckFailedException expected) {} + } + + @Test + public void testHashKeyAutogenerated() { + DynamoDBMapper mapper = new DynamoDBMapper(dynamo); + HashKeyAutoGenerated obj = new HashKeyAutoGenerated(); + obj.setOtherAttribute("blah"); + obj.setRangeKey("" + System.currentTimeMillis()); + + assertNull(obj.getKey()); + assertNotNull(obj.getRangeKey()); + mapper.save(obj); + assertNotNull(obj.getKey()); + assertNotNull(obj.getRangeKey()); + + HashKeyAutoGenerated other = mapper.load(HashKeyAutoGenerated.class, obj.getKey(), obj.getRangeKey()); + assertEquals(other, obj); + } + + @DynamoDBTable(tableName = "aws-java-sdk-string-range") + public static class RangeKeyAutoGenerated { + + private String key; + private String rangeKey; + private String otherAttribute; + + @DynamoDBHashKey + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + @DynamoDBAutoGeneratedKey + @DynamoDBRangeKey + public String getRangeKey() { + return rangeKey; + } + + public void setRangeKey(String rangeKey) { + this.rangeKey = rangeKey; + } + + public String getOtherAttribute() { + return otherAttribute; + } + + public void setOtherAttribute(String otherAttribute) { + this.otherAttribute = otherAttribute; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((key == null) ? 0 : key.hashCode()); + result = prime * result + ((otherAttribute == null) ? 0 : otherAttribute.hashCode()); + result = prime * result + ((rangeKey == null) ? 0 : rangeKey.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) + return true; + if ( obj == null ) + return false; + if ( getClass() != obj.getClass() ) + return false; + RangeKeyAutoGenerated other = (RangeKeyAutoGenerated) obj; + if ( key == null ) { + if ( other.key != null ) + return false; + } else if ( !key.equals(other.key) ) + return false; + if ( otherAttribute == null ) { + if ( other.otherAttribute != null ) + return false; + } else if ( !otherAttribute.equals(other.otherAttribute) ) + return false; + if ( rangeKey == null ) { + if ( other.rangeKey != null ) + return false; + } else if ( !rangeKey.equals(other.rangeKey) ) + return false; + return true; + } + } + + @Test + public void testRangeKeyAutogenerated() { + DynamoDBMapper mapper = new DynamoDBMapper(dynamo); + RangeKeyAutoGenerated obj = new RangeKeyAutoGenerated(); + obj.setOtherAttribute("blah"); + obj.setKey("" + System.currentTimeMillis()); + + assertNotNull(obj.getKey()); + assertNull(obj.getRangeKey()); + mapper.save(obj); + assertNotNull(obj.getKey()); + assertNotNull(obj.getRangeKey()); + + RangeKeyAutoGenerated other = mapper.load(RangeKeyAutoGenerated.class, obj.getKey(), obj.getRangeKey()); + assertEquals(other, obj); + } + + @DynamoDBTable(tableName = TABLE_NAME) + public static class NothingAutoGenerated { + + private String key; + private String rangeKey; + private String otherAttribute; + + @DynamoDBHashKey + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + @DynamoDBRangeKey + public String getRangeKey() { + return rangeKey; + } + + public void setRangeKey(String rangeKey) { + this.rangeKey = rangeKey; + } + + public String getOtherAttribute() { + return otherAttribute; + } + + public void setOtherAttribute(String otherAttribute) { + this.otherAttribute = otherAttribute; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((key == null) ? 0 : key.hashCode()); + result = prime * result + ((otherAttribute == null) ? 0 : otherAttribute.hashCode()); + result = prime * result + ((rangeKey == null) ? 0 : rangeKey.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) + return true; + if ( obj == null ) + return false; + if ( getClass() != obj.getClass() ) + return false; + NothingAutoGenerated other = (NothingAutoGenerated) obj; + if ( key == null ) { + if ( other.key != null ) + return false; + } else if ( !key.equals(other.key) ) + return false; + if ( otherAttribute == null ) { + if ( other.otherAttribute != null ) + return false; + } else if ( !otherAttribute.equals(other.otherAttribute) ) + return false; + if ( rangeKey == null ) { + if ( other.rangeKey != null ) + return false; + } else if ( !rangeKey.equals(other.rangeKey) ) + return false; + return true; + } + } + + @Test + public void testNothingAutogenerated() { + DynamoDBMapper mapper = new DynamoDBMapper(dynamo); + NothingAutoGenerated obj = new NothingAutoGenerated(); + obj.setOtherAttribute("blah"); + obj.setKey("" + System.currentTimeMillis()); + obj.setRangeKey("" + System.currentTimeMillis()); + + assertNotNull(obj.getKey()); + assertNotNull(obj.getRangeKey()); + mapper.save(obj); + assertNotNull(obj.getKey()); + assertNotNull(obj.getRangeKey()); + + NothingAutoGenerated other = mapper.load(NothingAutoGenerated.class, obj.getKey(), obj.getRangeKey()); + assertEquals(other, obj); + } + + @Test + public void testNothingAutogeneratedErrors() { + DynamoDBMapper mapper = new DynamoDBMapper(dynamo); + NothingAutoGenerated obj = new NothingAutoGenerated(); + + try { + mapper.save(obj); + fail("Expected a mapping exception"); + } catch (DynamoDBMappingException expected) { + } + + obj.setKey("" + System.currentTimeMillis()); + try { + mapper.save(obj); + fail("Expected a mapping exception"); + } catch (DynamoDBMappingException expected) { + } + + obj.setRangeKey("" + System.currentTimeMillis()); + obj.setKey(null); + try { + mapper.save(obj); + fail("Expected a mapping exception"); + } catch (DynamoDBMappingException expected) { + } + + obj.setRangeKey(""); + obj.setKey("" + System.currentTimeMillis()); + try { + mapper.save(obj); + fail("Expected a mapping exception"); + } catch (DynamoDBMappingException expected) { + } + + obj.setRangeKey("" + System.currentTimeMillis()); + mapper.save(obj); + } + + @DynamoDBTable(tableName = TABLE_NAME) + public static class HashKeyRangeKeyBothAutoGeneratedKeyOnly { + + private String key; + private String rangeKey; + + @DynamoDBAutoGeneratedKey + @DynamoDBHashKey + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + @DynamoDBAutoGeneratedKey + @DynamoDBRangeKey + public String getRangeKey() { + return rangeKey; + } + + public void setRangeKey(String rangeKey) { + this.rangeKey = rangeKey; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((key == null) ? 0 : key.hashCode()); + result = prime * result + ((rangeKey == null) ? 0 : rangeKey.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) + return true; + if ( obj == null ) + return false; + if ( getClass() != obj.getClass() ) + return false; + HashKeyRangeKeyBothAutoGeneratedKeyOnly other = (HashKeyRangeKeyBothAutoGeneratedKeyOnly) obj; + if ( key == null ) { + if ( other.key != null ) + return false; + } else if ( !key.equals(other.key) ) + return false; + if ( rangeKey == null ) { + if ( other.rangeKey != null ) + return false; + } else if ( !rangeKey.equals(other.rangeKey) ) + return false; + return true; + } + } + + @Test + public void testHashKeyRangeKeyBothAutogeneratedKeyOnly() { + DynamoDBMapper mapper = new DynamoDBMapper(dynamo); + HashKeyRangeKeyBothAutoGeneratedKeyOnly obj = new HashKeyRangeKeyBothAutoGeneratedKeyOnly(); + + assertNull(obj.getKey()); + assertNull(obj.getRangeKey()); + mapper.save(obj); + assertNotNull(obj.getKey()); + assertNotNull(obj.getRangeKey()); + + HashKeyRangeKeyBothAutoGeneratedKeyOnly other = mapper.load(HashKeyRangeKeyBothAutoGeneratedKeyOnly.class, obj.getKey(), + obj.getRangeKey()); + assertEquals(other, obj); + } + + @DynamoDBTable(tableName = TABLE_NAME) + public static class HashKeyAutoGeneratedKeyOnly { + + private String key; + private String rangeKey; + + @DynamoDBAutoGeneratedKey + @DynamoDBHashKey + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + @DynamoDBRangeKey + public String getRangeKey() { + return rangeKey; + } + + public void setRangeKey(String rangeKey) { + this.rangeKey = rangeKey; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((key == null) ? 0 : key.hashCode()); + result = prime * result + ((rangeKey == null) ? 0 : rangeKey.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) + return true; + if ( obj == null ) + return false; + if ( getClass() != obj.getClass() ) + return false; + HashKeyAutoGeneratedKeyOnly other = (HashKeyAutoGeneratedKeyOnly) obj; + if ( key == null ) { + if ( other.key != null ) + return false; + } else if ( !key.equals(other.key) ) + return false; + if ( rangeKey == null ) { + if ( other.rangeKey != null ) + return false; + } else if ( !rangeKey.equals(other.rangeKey) ) + return false; + return true; + } + + } + + @Test + public void testHashKeyAutogeneratedKeyOnly() { + DynamoDBMapper mapper = new DynamoDBMapper(dynamo); + HashKeyAutoGeneratedKeyOnly obj = new HashKeyAutoGeneratedKeyOnly(); + obj.setRangeKey("" + System.currentTimeMillis()); + + assertNull(obj.getKey()); + assertNotNull(obj.getRangeKey()); + mapper.save(obj); + assertNotNull(obj.getKey()); + assertNotNull(obj.getRangeKey()); + + HashKeyAutoGeneratedKeyOnly other = mapper.load(HashKeyAutoGeneratedKeyOnly.class, obj.getKey(), obj.getRangeKey()); + assertEquals(other, obj); + } + + @DynamoDBTable(tableName = TABLE_NAME) + public static class RangeKeyAutoGeneratedKeyOnly { + + private String key; + private String rangeKey; + + @DynamoDBHashKey + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + @DynamoDBAutoGeneratedKey + @DynamoDBRangeKey + public String getRangeKey() { + return rangeKey; + } + + public void setRangeKey(String rangeKey) { + this.rangeKey = rangeKey; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((key == null) ? 0 : key.hashCode()); + result = prime * result + ((rangeKey == null) ? 0 : rangeKey.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) + return true; + if ( obj == null ) + return false; + if ( getClass() != obj.getClass() ) + return false; + RangeKeyAutoGeneratedKeyOnly other = (RangeKeyAutoGeneratedKeyOnly) obj; + if ( key == null ) { + if ( other.key != null ) + return false; + } else if ( !key.equals(other.key) ) + return false; + if ( rangeKey == null ) { + if ( other.rangeKey != null ) + return false; + } else if ( !rangeKey.equals(other.rangeKey) ) + return false; + return true; + } + + } + + @Test + public void testRangeKeyAutogeneratedKeyOnly() { + DynamoDBMapper mapper = new DynamoDBMapper(dynamo); + RangeKeyAutoGeneratedKeyOnly obj = new RangeKeyAutoGeneratedKeyOnly(); + obj.setKey("" + System.currentTimeMillis()); + + assertNotNull(obj.getKey()); + assertNull(obj.getRangeKey()); + mapper.save(obj); + assertNotNull(obj.getKey()); + assertNotNull(obj.getRangeKey()); + + RangeKeyAutoGeneratedKeyOnly other = mapper.load(RangeKeyAutoGeneratedKeyOnly.class, obj.getKey(), obj.getRangeKey()); + assertEquals(other, obj); + } + + @DynamoDBTable(tableName = TABLE_NAME) + public static class NothingAutoGeneratedKeyOnly { + + private String key; + private String rangeKey; + + @DynamoDBHashKey + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + @DynamoDBRangeKey + public String getRangeKey() { + return rangeKey; + } + + public void setRangeKey(String rangeKey) { + this.rangeKey = rangeKey; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((key == null) ? 0 : key.hashCode()); + result = prime * result + ((rangeKey == null) ? 0 : rangeKey.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) + return true; + if ( obj == null ) + return false; + if ( getClass() != obj.getClass() ) + return false; + NothingAutoGeneratedKeyOnly other = (NothingAutoGeneratedKeyOnly) obj; + if ( key == null ) { + if ( other.key != null ) + return false; + } else if ( !key.equals(other.key) ) + return false; + if ( rangeKey == null ) { + if ( other.rangeKey != null ) + return false; + } else if ( !rangeKey.equals(other.rangeKey) ) + return false; + return true; + } + } + + @Test + public void testNothingAutogeneratedKeyOnly() { + DynamoDBMapper mapper = new DynamoDBMapper(dynamo); + NothingAutoGeneratedKeyOnly obj = new NothingAutoGeneratedKeyOnly(); + obj.setKey("" + System.currentTimeMillis()); + obj.setRangeKey("" + System.currentTimeMillis()); + + assertNotNull(obj.getKey()); + assertNotNull(obj.getRangeKey()); + mapper.save(obj); + assertNotNull(obj.getKey()); + assertNotNull(obj.getRangeKey()); + + NothingAutoGeneratedKeyOnly other = mapper.load(NothingAutoGeneratedKeyOnly.class, obj.getKey(), obj.getRangeKey()); + assertEquals(other, obj); + } + + @Test + public void testNothingAutogeneratedKeyOnlyErrors() { + DynamoDBMapper mapper = new DynamoDBMapper(dynamo); + NothingAutoGeneratedKeyOnly obj = new NothingAutoGeneratedKeyOnly(); + + try { + mapper.save(obj); + fail("Expected a mapping exception"); + } catch (DynamoDBMappingException expected) { + } + + obj.setKey("" + System.currentTimeMillis()); + try { + mapper.save(obj); + fail("Expected a mapping exception"); + } catch (DynamoDBMappingException expected) { + } + + obj.setRangeKey("" + System.currentTimeMillis()); + obj.setKey(null); + try { + mapper.save(obj); + fail("Expected a mapping exception"); + } catch (DynamoDBMappingException expected) { + } + + obj.setRangeKey(""); + obj.setKey("" + System.currentTimeMillis()); + try { + mapper.save(obj); + fail("Expected a mapping exception"); + } catch (DynamoDBMappingException expected) { + } + + obj.setRangeKey("" + System.currentTimeMillis()); + mapper.save(obj); + } + + @Test + public void testIndexKeyWithAutogeneratedAnnotation_StillRequirePrimaryKeyValue() { + DynamoDBMapper mapper = new DynamoDBMapper(dynamo); + PrimaryKeysNotAutogenerated_IndexKeysAutogenerated obj = new PrimaryKeysNotAutogenerated_IndexKeysAutogenerated(); + + try { + mapper.save(obj); + fail("DynamoDBMappingException is expected."); + } catch (DynamoDBMappingException expected) { + } + + obj.setGsiHashKey("foo"); + obj.setGsiRangeKey("foo"); + try { + mapper.save(obj); + fail("DynamoDBMappingException is expected."); + } catch (DynamoDBMappingException expected) { + } + } + + @Test + public void testIndexKeyWithAutogeneratedAnnotation_AutogenerateIndexKeyValueIfNull() { + DynamoDBMapper mapper = new DynamoDBMapper(dynamo); + PrimaryKeysNotAutogenerated_IndexKeysAutogenerated obj = new PrimaryKeysNotAutogenerated_IndexKeysAutogenerated(); + + String randomPrimaryKeyValue = UUID.randomUUID().toString(); + obj.setKey(randomPrimaryKeyValue); + obj.setRangeKey(randomPrimaryKeyValue); + + assertNull(obj.getGsiHashKey()); + assertNull(obj.getGsiRangeKey()); + mapper.save(obj); + + // check in-memory value + assertNotNull(obj.getGsiHashKey()); + assertNotNull(obj.getGsiRangeKey()); + + PrimaryKeysNotAutogenerated_IndexKeysAutogenerated retrieved = mapper.load(obj); + assertEquals(obj, retrieved); + } + + @Test + public void testIndexKeyWithAutogeneratedAnnotation_DoNotAutogenerateIndexKeyValueIfAlreadySpecified() { + DynamoDBMapper mapper = new DynamoDBMapper(dynamo); + PrimaryKeysNotAutogenerated_IndexKeysAutogenerated obj = new PrimaryKeysNotAutogenerated_IndexKeysAutogenerated(); + + String randomValue = UUID.randomUUID().toString(); + obj.setKey(randomValue); + obj.setRangeKey(randomValue); + obj.setGsiHashKey(randomValue); + obj.setGsiRangeKey(randomValue); + mapper.save(obj); + + // check in-memory value + assertEquals(randomValue, obj.getGsiHashKey()); + assertEquals(randomValue, obj.getGsiRangeKey()); + + PrimaryKeysNotAutogenerated_IndexKeysAutogenerated retrieved = mapper.load(obj); + assertEquals(obj, retrieved); + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/AutoGeneratedTimestampIntegrationTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/AutoGeneratedTimestampIntegrationTest.java new file mode 100644 index 000000000000..d6ab01166125 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/AutoGeneratedTimestampIntegrationTest.java @@ -0,0 +1,412 @@ +/* + * 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.mapper.dynamodb.mapper; + +import static org.junit.Assert.assertEquals; + +import software.amazon.awssdk.mapper.dynamodb.pojos.AutoKeyAndVal; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAutoGenerateStrategy; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAutoGeneratedTimestamp; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.SaveBehavior; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; +import software.amazon.awssdk.mapper.dynamodb.pojos.KeyAndVal; + +import java.util.Calendar; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import org.junit.Test; + +/** + * Tests updating component attribute fields correctly. + */ +public class AutoGeneratedTimestampIntegrationTest extends AbstractKeyAndValIntegrationTestCase { + + /** + * Test using {@code Calendar}. + */ + @Test + public void testCalendarType() { + final KeyAndCalendarTimestamp object = new KeyAndCalendarTimestamp(); + assertBeforeAndAfterChange(true, object); + } + + /** + * Test using {@code Date}. + */ + @Test + public void testDateType() { + final KeyAndDateTimestamp object = new KeyAndDateTimestamp(); + + assertBeforeAndAfterChange(true, object); + } + + /** + * Test using a {@code Long}. + */ + @Test + public void testLongType() { + final KeyAndLongTimestamp object = new KeyAndLongTimestamp(); + + assertBeforeAndAfterChange(true, object); + } + + /** + * Test {@code DynamoDBAutoGenerateStrategy} of {@code ALWAYS}. + */ + @Test + public void testAlwaysStrategy() { + final KeyAndDateTimestamp object = new KeyAndDateTimestamp(); + + assertBeforeAndAfterChange(true, object); + assertBeforeAndAfterChange(true, object); + assertBeforeAndAfterChange(true, object); + } + + /** + * Test {@code DynamoDBAutoGenerateStrategy} of {@code ALWAYS}. + */ + @Test + public void testAlwaysStrategyUpdateSkipNullAttribute() { + setUpTest(SaveBehavior.UPDATE_SKIP_NULL_ATTRIBUTES); + + final KeyAndDateTimestamp object = new KeyAndDateTimestamp(); + + assertBeforeAndAfterChange(true, object); + assertBeforeAndAfterChange(true, object); + assertBeforeAndAfterChange(true, object); + } + + /** + * Test {@code DynamoDBAutoGenerateStrategy} of {@code CREATE}. + */ + @Test + public void testCreateStrategy() { + final KeyAndOnCreateDateTimestamp object = new KeyAndOnCreateDateTimestamp(); + + assertBeforeAndAfterChange(true, object); + assertBeforeAndAfterChange(false, object); + assertBeforeAndAfterChange(false, object); + } + + /** + * Test {@code DynamoDBAutoGenerateStrategy} of {@code CREATE}. + */ + @Test + public void testCreateStrategyUpdateSkipNullAttributes() { + setUpTest(SaveBehavior.UPDATE_SKIP_NULL_ATTRIBUTES); + + final KeyAndOnCreateDateTimestamp object = new KeyAndOnCreateDateTimestamp(); + + assertBeforeAndAfterChange(true, object); + assertBeforeAndAfterChange(false, object); + assertBeforeAndAfterChange(false, object); + } + + /** + * Test {@code DynamoDBAutoGenerateStrategy} of {@code ALWAYS}. + */ + @Test + public void testAlwaysNoKey() { + final NoKeyAndOnAlwaysDateTimestamp object = new NoKeyAndOnAlwaysDateTimestamp(); + object.setKey(UUID.randomUUID().toString()); + + assertBeforeAndAfterChange(true, object); + assertBeforeAndAfterChange(true, object); + assertBeforeAndAfterChange(true, object); + } + + /** + * Test {@code DynamoDBAutoGenerateStrategy} of {@code ALWAYS}. + */ + @Test + public void testAlwaysNoKeyUpdateSkipNullAttributes() { + setUpTest(SaveBehavior.UPDATE_SKIP_NULL_ATTRIBUTES); + + final NoKeyAndOnAlwaysDateTimestamp object = new NoKeyAndOnAlwaysDateTimestamp(); + object.setKey(UUID.randomUUID().toString()); + + assertBeforeAndAfterChange(true, object); + assertBeforeAndAfterChange(true, object); + assertBeforeAndAfterChange(true, object); + } + + /** + * Test {@code DynamoDBAutoGenerateStrategy} of {@code CREATE}. + */ + @Test + public void testCreateNoKey() { + final NoKeyAndOnCreateDateTimestamp object = new NoKeyAndOnCreateDateTimestamp(); + object.setKey(UUID.randomUUID().toString()); + + assertBeforeAndAfterChange(true, object); + assertBeforeAndAfterChange(false, object); + assertBeforeAndAfterChange(false, object); + } + + /** + * Test {@code DynamoDBAutoGenerateStrategy} of {@code CREATE}. + */ + @Test + public void testCreateNoKeyUpdateSkipNullAttributes() { + setUpTest(SaveBehavior.UPDATE_SKIP_NULL_ATTRIBUTES); + + final NoKeyAndOnCreateDateTimestamp object = new NoKeyAndOnCreateDateTimestamp(); + object.setKey(UUID.randomUUID().toString()); + + assertBeforeAndAfterChange(false, object); + assertBeforeAndAfterChange(false, object); + assertBeforeAndAfterChange(false, object); + } + + /** + * Test the batch save. + */ + @Test + public void testAlwaysOnBatchSave() { + final List objects = new ArrayList(); + for (int i = 0; i < 10; i++) { + objects.add(new KeyAndDateTimestamp()); + } + + assertBeforeAndAfterChange(true, objects); + assertBeforeAndAfterChange(true, objects); + assertBeforeAndAfterChange(true, objects); + } + + /** + * Test the batch save. + */ + @Test + public void testAlwaysOnBatchSaveUpdateSkipNullAttributes() { + setUpTest(SaveBehavior.UPDATE_SKIP_NULL_ATTRIBUTES); + + final List objects = new ArrayList(); + for (int i = 0; i < 10; i++) { + objects.add(new KeyAndDateTimestamp()); + } + + assertBeforeAndAfterChange(true, objects); + assertBeforeAndAfterChange(true, objects); + assertBeforeAndAfterChange(true, objects); + } + + /** + * Test the batch save. + */ + @Test + public void testCreateOnBatchSave() { + final List objects = new ArrayList(); + for (int i = 0; i < 10; i++) { + objects.add(new KeyAndOnCreateDateTimestamp()); + } + + assertBeforeAndAfterChange(true, objects); + assertBeforeAndAfterChange(false, objects); + assertBeforeAndAfterChange(false, objects); + } + + /** + * Test the batch save. + */ + @Test + public void testCreateOnBatchSaveUpdateSkipNullAttributes() { + setUpTest(SaveBehavior.UPDATE_SKIP_NULL_ATTRIBUTES); + + final List objects = new ArrayList(); + for (int i = 0; i < 10; i++) { + objects.add(new KeyAndOnCreateDateTimestamp()); + } + + assertBeforeAndAfterChange(true, objects); + assertBeforeAndAfterChange(false, objects); + assertBeforeAndAfterChange(false, objects); + } + + /** + * Test the batch save. + */ + @Test + public void testAlwaysNoKeyOnBatchSave() { + final List objects = new ArrayList(); + for (int i = 0; i < 10; i++) { + final NoKeyAndOnAlwaysDateTimestamp object = new NoKeyAndOnAlwaysDateTimestamp(); + object.setKey(UUID.randomUUID().toString()); + objects.add(object); + } + + assertBeforeAndAfterChange(true, objects); + assertBeforeAndAfterChange(true, objects); + assertBeforeAndAfterChange(true, objects); + } + + /** + * Test the batch save. + */ + @Test + public void testAlwaysNoKeyOnBatchSaveUpdateSkipNullAttributes() { + setUpTest(SaveBehavior.UPDATE_SKIP_NULL_ATTRIBUTES); + + final List objects = new ArrayList(); + for (int i = 0; i < 10; i++) { + final NoKeyAndOnAlwaysDateTimestamp object = new NoKeyAndOnAlwaysDateTimestamp(); + object.setKey(UUID.randomUUID().toString()); + objects.add(object); + } + + assertBeforeAndAfterChange(true, objects); + assertBeforeAndAfterChange(true, objects); + assertBeforeAndAfterChange(true, objects); + } + + /** + * Test the batch save. + */ + @Test + public void testCreateNoKeyOnBatchSave() { + final List objects = new ArrayList(); + for (int i = 0; i < 10; i++) { + final NoKeyAndOnCreateDateTimestamp object = new NoKeyAndOnCreateDateTimestamp(); + object.setKey(UUID.randomUUID().toString()); + objects.add(object); + } + + assertBeforeAndAfterChange(true, objects); + assertBeforeAndAfterChange(false, objects); + assertBeforeAndAfterChange(false, objects); + } + + /** + * Test the batch save. + */ + @Test + public void testCreateNoKeyOnBatchSaveUpdateSkipNullAttributes() { + setUpTest(SaveBehavior.UPDATE_SKIP_NULL_ATTRIBUTES); + + final List objects = new ArrayList(); + for (int i = 0; i < 10; i++) { + final NoKeyAndOnCreateDateTimestamp object = new NoKeyAndOnCreateDateTimestamp(); + object.setKey(UUID.randomUUID().toString()); + objects.add(object); + } + + assertBeforeAndAfterChange(false, objects); + assertBeforeAndAfterChange(false, objects); + assertBeforeAndAfterChange(false, objects); + } + + /** + * An object with {@code Calendar}. + */ + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class KeyAndCalendarTimestamp extends AutoKeyAndVal { + @DynamoDBAutoGeneratedTimestamp + public Calendar getVal() { + return super.getVal(); + } + public void setVal(final Calendar val) { + super.setVal(val); + } + } + + /** + * An object with {@code Date}. + */ + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class KeyAndDateTimestamp extends AutoKeyAndVal { + @DynamoDBAutoGeneratedTimestamp + public Date getVal() { + return super.getVal(); + } + public void setVal(final Date val) { + super.setVal(val); + } + } + + /** + * An object with {@code Long}. + */ + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class KeyAndLongTimestamp extends AutoKeyAndVal { + @DynamoDBAutoGeneratedTimestamp + public Long getVal() { + return super.getVal(); + } + public void setVal(final Long val) { + super.setVal(val); + } + } + + /** + * An object with {@code Date} only on {@code CREATE}. + */ + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class KeyAndOnCreateDateTimestamp extends AutoKeyAndVal { + @DynamoDBAutoGeneratedTimestamp(strategy=DynamoDBAutoGenerateStrategy.CREATE) + public Date getVal() { + return super.getVal(); + } + public void setVal(final Date val) { + super.setVal(val); + } + } + + /** + * An object with {@code Date} not auto-generted key. + */ + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class NoKeyAndOnAlwaysDateTimestamp extends KeyAndVal { + @DynamoDBHashKey + public String getKey() { + return super.getKey(); + } + public void setKey(final String key) { + super.setKey(key); + } + @DynamoDBAutoGeneratedTimestamp + public Date getVal() { + return super.getVal(); + } + public void setVal(final Date val) { + super.setVal(val); + } + } + + /** + * An object with {@code Date} not auto-generted key. + */ + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class NoKeyAndOnCreateDateTimestamp extends KeyAndVal { + @DynamoDBHashKey + public String getKey() { + return super.getKey(); + } + public void setKey(final String key) { + super.setKey(key); + } + @DynamoDBAutoGeneratedTimestamp(strategy=DynamoDBAutoGenerateStrategy.CREATE) + public Date getVal() { + return super.getVal(); + } + public void setVal(final Date val) { + super.setVal(val); + } + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/BatchWriteTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/BatchWriteTest.java new file mode 100644 index 000000000000..eb33d47c9a6b --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/BatchWriteTest.java @@ -0,0 +1,415 @@ +/* + * 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.mapper.dynamodb.mapper; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.mapper.dynamodb.LocalDynamoDBTestBase; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper.FailedBatch; +import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition; +import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest; +import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement; +import software.amazon.awssdk.services.dynamodb.model.KeyType; +import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType; +import software.amazon.awssdk.mapper.dynamodb.pojos.BinaryAttributeByteBufferClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.RangeKeyClass; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * Tests batch write calls + */ +public class BatchWriteTest extends LocalDynamoDBTestBase { + + // We don't start with the current system millis like other tests because + // it's out of the range of some data types + private static int start = 1; + private static int byteStart = 1; + private static int startKeyDebug = 1; + private static long startKey = System.currentTimeMillis(); + private static DynamoDbClient dynamo; + + @BeforeClass + public static void setUp() throws Exception { + dynamo = client(); + dynamo.createTable(hashKeyTable("aws-java-sdk-util", "key")); + dynamo.createTable(hashAndRangeKeyTable("aws-java-sdk-range-test", "key", "rangeKey")); + } + + private static CreateTableRequest hashKeyTable(String tableName, String hashKey) { + return CreateTableRequest.builder() + .tableName(tableName) + .keySchema(KeySchemaElement.builder().attributeName(hashKey).keyType(KeyType.HASH).build()) + .attributeDefinitions(AttributeDefinition.builder().attributeName(hashKey) + .attributeType(ScalarAttributeType.S).build()) + .provisionedThroughput(DEFAULT_PROVISIONED_THROUGHPUT) + .build(); + } + + private static CreateTableRequest hashAndRangeKeyTable(String tableName, String hashKey, String rangeKey) { + return CreateTableRequest.builder() + .tableName(tableName) + .keySchema(KeySchemaElement.builder().attributeName(hashKey).keyType(KeyType.HASH).build(), + KeySchemaElement.builder().attributeName(rangeKey).keyType(KeyType.RANGE).build()) + .attributeDefinitions(AttributeDefinition.builder().attributeName(hashKey) + .attributeType(ScalarAttributeType.N).build(), + AttributeDefinition.builder().attributeName(rangeKey) + .attributeType(ScalarAttributeType.N).build()) + .provisionedThroughput(DEFAULT_PROVISIONED_THROUGHPUT) + .build(); + } + + @Test + public void testBatchSave() throws Exception { + List objs = new ArrayList(); + for ( int i = 0; i < 40; i++ ) { + NumberSetAttributeClass obj = getUniqueNumericObject(); + objs.add(obj); + } + + DynamoDBMapper mapper = new DynamoDBMapper(dynamo); + List failedBatches = mapper.batchSave(objs); + + assertEquals(0, failedBatches.size()); + + for (NumberSetAttributeClass obj : objs) { + NumberSetAttributeClass loaded = mapper.load(NumberSetAttributeClass.class, obj.getKey()); + assertEquals(obj, loaded); + } + } + + @Test + public void testBatchSaveAsArray() throws Exception { + List objs = new ArrayList(); + for ( int i = 0; i < 40; i++ ) { + NumberSetAttributeClass obj = getUniqueNumericObject(); + objs.add(obj); + } + + DynamoDBMapper mapper = new DynamoDBMapper(dynamo); + NumberSetAttributeClass[] objsArray = objs.toArray(new NumberSetAttributeClass[objs.size()]); + mapper.batchSave((Object[])objsArray); + + for (NumberSetAttributeClass obj : objs) { + NumberSetAttributeClass loaded = mapper.load(NumberSetAttributeClass.class, obj.getKey()); + assertEquals(obj, loaded); + } + } + + @Test + public void testBatchSaveAsListFromArray() throws Exception { + List objs = new ArrayList(); + for ( int i = 0; i < 40; i++ ) { + NumberSetAttributeClass obj = getUniqueNumericObject(); + objs.add(obj); + } + + DynamoDBMapper mapper = new DynamoDBMapper(dynamo); + NumberSetAttributeClass[] objsArray = objs.toArray(new NumberSetAttributeClass[objs.size()]); + mapper.batchSave(Arrays.asList(objsArray)); + + for (NumberSetAttributeClass obj : objs) { + NumberSetAttributeClass loaded = mapper.load(NumberSetAttributeClass.class, obj.getKey()); + assertEquals(obj, loaded); + } + } + + @Test + public void testBatchDelete() throws Exception { + List objs = new ArrayList(); + for ( int i = 0; i < 40; i++ ) { + NumberSetAttributeClass obj = getUniqueNumericObject(); + objs.add(obj); + } + + DynamoDBMapper mapper = new DynamoDBMapper(dynamo); + mapper.batchSave(objs); + + for ( NumberSetAttributeClass obj : objs ) { + NumberSetAttributeClass loaded = mapper.load(NumberSetAttributeClass.class, obj.getKey()); + assertEquals(obj, loaded); + } + + // Delete the odd ones + int i = 0; + List toDelete = new LinkedList(); + for ( NumberSetAttributeClass obj : objs ) { + if (i++ % 2 == 0) toDelete.add(obj); + } + + mapper.batchDelete(toDelete); + + i = 0; + for ( NumberSetAttributeClass obj : objs ) { + NumberSetAttributeClass loaded = mapper.load(NumberSetAttributeClass.class, obj.getKey()); + if (i++ % 2 == 0) { + assertNull(loaded); + } else { + assertEquals(obj, loaded); + } + } + } + + @Test + public void testMultipleTables() throws Exception { + + List objs = new ArrayList(); + int numItems = 10; + for ( int i = 0; i < numItems; i++ ) { + NumberSetAttributeClass obj = getUniqueNumericObject(); + objs.add(obj); + } + for ( int i = 0; i < numItems; i++ ) { + RangeKeyClass obj = getUniqueRangeKeyObject(); + objs.add(obj); + } + Collections.shuffle(objs); + + DynamoDBMapper mapper = new DynamoDBMapper(dynamo); + List failedBatches = mapper.batchSave(objs); + assertEquals(failedBatches.size(), 0); + + for ( Object obj : objs ) { + Object loaded = null; + if ( obj instanceof NumberSetAttributeClass ) { + loaded = mapper.load(NumberSetAttributeClass.class, ((NumberSetAttributeClass) obj).getKey()); + } else if ( obj instanceof RangeKeyClass ) { + loaded = mapper.load(RangeKeyClass.class, ((RangeKeyClass) obj).getKey(), + ((RangeKeyClass) obj).getRangeKey()); + } else { + fail(); + } + assertEquals(obj, loaded); + } + + // Delete the odd ones + int i = 0; + List toDelete = new LinkedList(); + for ( Object obj : objs ) { + if (i++ % 2 == 0) toDelete.add(obj); + } + + // And add a bunch of new ones + List toSave = new LinkedList(); + for ( i = 0; i < numItems; i++ ) { + if ( i % 2 == 0 ) + toSave.add(getUniqueNumericObject()); + else + toSave.add(getUniqueRangeKeyObject()); + } + + failedBatches = mapper.batchWrite(toSave, toDelete); + assertEquals(0, failedBatches.size()); + + i = 0; + for ( Object obj : objs ) { + Object loaded = null; + if ( obj instanceof NumberSetAttributeClass ) { + loaded = mapper.load(NumberSetAttributeClass.class, ((NumberSetAttributeClass) obj).getKey()); + } else if ( obj instanceof RangeKeyClass ) { + loaded = mapper.load(RangeKeyClass.class, ((RangeKeyClass) obj).getKey(), + ((RangeKeyClass) obj).getRangeKey()); + } else { + fail(); + } + + if (i++ % 2 == 0) { + assertNull(loaded); + } else { + assertEquals(obj, loaded); + } + } + + for ( Object obj : toSave ) { + Object loaded = null; + if ( obj instanceof NumberSetAttributeClass ) { + loaded = mapper.load(NumberSetAttributeClass.class, ((NumberSetAttributeClass) obj).getKey()); + } else if ( obj instanceof RangeKeyClass ) { + loaded = mapper.load(RangeKeyClass.class, ((RangeKeyClass) obj).getKey(), + ((RangeKeyClass) obj).getRangeKey()); + } else { + fail(); + } + assertEquals(obj, loaded); + } + } + + /** + * Test whether it finish processing all the items even if the first batch is failed. + */ + @Test + public void testErrorHandling() { + + List objs = new ArrayList(); + int numItems = 25; + + for (int i = 0; i < numItems; i++) { + NoSuchTableClass obj = getuniqueBadObject(); + objs.add(obj); + } + + for (int i = 0; i < numItems; i++) { + RangeKeyClass obj = getUniqueRangeKeyObject(); + objs.add(obj); + } + + DynamoDBMapper mapper = new DynamoDBMapper(dynamo); + + // The failed batch + List failedBatches = mapper.batchSave(objs); + assertEquals(1, failedBatches.size()); + assertEquals(numItems, failedBatches.get(0).getUnprocessedItems().get("tableNotExist").size()); + + // The second batch succeeds, get them back + for (Object obj : objs.subList(25, 50)) { + RangeKeyClass loaded = mapper.load(RangeKeyClass.class, ((RangeKeyClass) obj).getKey(), ((RangeKeyClass) obj).getRangeKey()); + assertEquals(obj, loaded); + } + } + + /** + * Test whether we can split large batch request into small pieces. + */ + // DynamoDB changed their error for requests that are too large from a + // 413 (RequestEntityTooLarge) to a generic 400 (ValidationException), so + // the mapper's batch-splitting logic is broken. Not sure there's a good + // fix client-side without the service changing back to 413 so we can + // distinguish this case from other ValidationExceptions. + // @Test + public void testLargeRequestEntity() { + + // The total batch size is beyond 1M, test whether our client can split + // the batch correctly + List objs = new ArrayList(); + + int numItems = 25; + final int CONTENT_LENGTH = 1024 * 25; + + for (int i = 0; i < numItems; i++) { + BinaryAttributeByteBufferClass obj = getUniqueByteBufferObject(CONTENT_LENGTH); + objs.add(obj); + } + + DynamoDBMapper mapper = new DynamoDBMapper(dynamo); + List failedBatches = mapper.batchSave(objs); + assertEquals(0, failedBatches.size()); + + // Get these objects back + for (BinaryAttributeByteBufferClass obj : objs) { + BinaryAttributeByteBufferClass loaded = mapper.load(BinaryAttributeByteBufferClass.class, obj.getKey()); + assertEquals(obj, loaded); + } + + // There are three super large item together with some small ones, test + // whether we can successfully + // save these small items. + objs.clear(); + numItems = 10; + List largeObjs = new ArrayList(); + + // Put three super large item(beyond 64k) + largeObjs.add(getUniqueByteBufferObject(CONTENT_LENGTH * 30)); + largeObjs.add(getUniqueByteBufferObject(CONTENT_LENGTH * 30)); + largeObjs.add(getUniqueByteBufferObject(CONTENT_LENGTH * 30)); + for (int i = 0; i < numItems - 3; i++) { + BinaryAttributeByteBufferClass obj = getUniqueByteBufferObject(CONTENT_LENGTH / 25); + objs.add(obj); + } + + objs.addAll(largeObjs); + + failedBatches = mapper.batchSave(objs); + assertEquals(3, failedBatches.size()); + objs.removeAll(largeObjs); + + // Get these small objects back + for (BinaryAttributeByteBufferClass obj : objs) { + BinaryAttributeByteBufferClass loaded = mapper.load(BinaryAttributeByteBufferClass.class, obj.getKey()); + assertEquals(obj, loaded); + } + + // The whole batch is super large objects, none of them will be + // processed + largeObjs.clear(); + for (int i = 0; i < 5; i++) { + BinaryAttributeByteBufferClass obj = getUniqueByteBufferObject(CONTENT_LENGTH * 30); + largeObjs.add(obj); + } + failedBatches = mapper.batchSave(largeObjs); + assertEquals(5, failedBatches.size()); + } + + + + private NoSuchTableClass getuniqueBadObject() { + NoSuchTableClass obj = new NoSuchTableClass(); + obj.setKey(String.valueOf(startKeyDebug++)); + return obj; + } + + private NumberSetAttributeClass getUniqueNumericObject() { + NumberSetAttributeClass obj = new NumberSetAttributeClass(); + obj.setKey(String.valueOf(startKeyDebug++)); + obj.setBigDecimalAttribute(toSet(new BigDecimal(startKey++), new BigDecimal(startKey++), new BigDecimal(startKey++))); + obj.setBigIntegerAttribute(toSet(new BigInteger("" + startKey++), new BigInteger("" + startKey++), new BigInteger("" + startKey++))); + obj.setByteObjectAttribute(toSet(new Byte(nextByte()), new Byte(nextByte()), new Byte(nextByte()))); + obj.setDoubleObjectAttribute(toSet(new Double("" + start++), new Double("" + start++), new Double("" + start++))); + obj.setFloatObjectAttribute(toSet(new Float("" + start++), new Float("" + start++), new Float("" + start++))); + obj.setIntegerAttribute(toSet(new Integer("" + start++), new Integer("" + start++), new Integer("" + start++))); + obj.setLongObjectAttribute(toSet(new Long("" + start++), new Long("" + start++), new Long("" + start++))); + obj.setBooleanAttribute(toSet(true, false)); + obj.setDateAttribute(toSet(new Date(startKey++), new Date(startKey++), new Date(startKey++))); + Set cals = new HashSet(); + for ( Date d : obj.getDateAttribute() ) { + Calendar cal = GregorianCalendar.getInstance(); + cal.setTime(d); + cals.add(cal); + } + obj.setCalendarAttribute(toSet(cals)); + return obj; + } + + private RangeKeyClass getUniqueRangeKeyObject() { + RangeKeyClass obj = new RangeKeyClass(); + obj.setKey(startKey++); + obj.setIntegerAttribute(toSet(start++, start++, start++)); + obj.setBigDecimalAttribute(new BigDecimal(startKey++)); + obj.setRangeKey(start++); + obj.setStringAttribute("" + startKey++); + obj.setStringSetAttribute(toSet("" + startKey++, "" + startKey++, "" + startKey++)); + return obj; + } + + private String nextByte() { + return "" + byteStart++ % Byte.MAX_VALUE; + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/BinaryAttributesIntegrationTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/BinaryAttributesIntegrationTest.java new file mode 100644 index 000000000000..459bbe59e229 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/BinaryAttributesIntegrationTest.java @@ -0,0 +1,278 @@ +/* + * 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.mapper.dynamodb.mapper; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.junit.BeforeClass; +import org.junit.Test; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperIntegrationTestBase; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; +import software.amazon.awssdk.mapper.dynamodb.pojos.BinaryAttributeByteArrayClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.BinaryAttributeByteBufferClass; + +/** + * Tests simple string attributes + */ +public class BinaryAttributesIntegrationTest extends DynamoDBMapperIntegrationTestBase { + + private static final String BINARY_ATTRIBUTE = "binaryAttribute"; + private static final String BINARY_SET_ATTRIBUTE = "binarySetAttribute"; + private static final List> attrs = new LinkedList>(); + private static final int contentLength = 512; + // Test data + static { + Map attr = new HashMap(); + attr.put(KEY_NAME, AttributeValue.builder().s("" + startKey++).build()); + attr.put(BINARY_ATTRIBUTE, AttributeValue.builder().b(SdkBytes.fromByteBuffer(ByteBuffer.wrap(generateByteArray(contentLength)))).build()); + attr.put(BINARY_SET_ATTRIBUTE, AttributeValue.builder(). + bs(SdkBytes.fromByteBuffer(ByteBuffer.wrap(generateByteArray(contentLength))), + SdkBytes.fromByteBuffer(ByteBuffer.wrap(generateByteArray(contentLength + 1)))).build()); + attrs.add(attr); + + }; + + @BeforeClass + public static void setUp() throws Exception { + DynamoDBMapperIntegrationTestBase.setUp(); + + // Insert the data + for ( Map attr : attrs ) { + dynamo.putItem(PutItemRequest.builder().tableName(TABLE_NAME).item(attr).build()); + } + } + + @Test + public void testLoad() throws Exception { + DynamoDBMapper util = new DynamoDBMapper(dynamo); + + for ( Map attr : attrs ) { + // test BinaryAttributeClass + BinaryAttributeByteBufferClass x = util.load(BinaryAttributeByteBufferClass.class, attr.get(KEY_NAME).s()); + assertEquals(x.getKey(), attr.get(KEY_NAME).s()); + assertEquals(x.getBinaryAttribute(), ByteBuffer.wrap(generateByteArray(contentLength))); + assertTrue(x.getBinarySetAttribute().contains(ByteBuffer.wrap(generateByteArray(contentLength)))); + assertTrue(x.getBinarySetAttribute().contains(ByteBuffer.wrap(generateByteArray(contentLength + 1)))); + + // test BinaryAttributeByteArrayClass + BinaryAttributeByteArrayClass y = util.load(BinaryAttributeByteArrayClass.class, attr.get(KEY_NAME).s()); + assertEquals(y.getKey(), attr.get(KEY_NAME).s()); + assertTrue(Arrays.equals(y.getBinaryAttribute(), (generateByteArray(contentLength)))); + assertEquals(2, y.getBinarySetAttribute().size()); + assertTrue(setContainsBytes(y.getBinarySetAttribute(), generateByteArray(contentLength))); + assertTrue(setContainsBytes(y.getBinarySetAttribute(), generateByteArray(contentLength+1))); + } + + } + + @Test + public void testSave() { + // test BinaryAttributeClass + List byteBufferObjs = new ArrayList(); + for ( int i = 0; i < 5; i++ ) { + BinaryAttributeByteBufferClass obj = getUniqueByteBufferObject(contentLength); + byteBufferObjs.add(obj); + } + + DynamoDBMapper util = new DynamoDBMapper(dynamo); + for (BinaryAttributeByteBufferClass obj : byteBufferObjs) { + util.save(obj); + } + + for (BinaryAttributeByteBufferClass obj : byteBufferObjs) { + BinaryAttributeByteBufferClass loaded = util.load(BinaryAttributeByteBufferClass.class, obj.getKey()); + assertEquals(loaded.getKey(), obj.getKey()); + assertEquals(loaded.getBinaryAttribute(), ByteBuffer.wrap(generateByteArray(contentLength))); + assertTrue(loaded.getBinarySetAttribute().contains(ByteBuffer.wrap(generateByteArray(contentLength)))); + } + + // test BinaryAttributeByteArrayClass + List bytesObjs = new ArrayList(); + for ( int i = 0; i < 5; i++ ) { + BinaryAttributeByteArrayClass obj = getUniqueBytesObject(contentLength); + bytesObjs.add(obj); + } + + for (BinaryAttributeByteArrayClass obj : bytesObjs) { + util.save(obj); + } + + for (BinaryAttributeByteArrayClass obj : bytesObjs) { + BinaryAttributeByteArrayClass loaded = util.load(BinaryAttributeByteArrayClass.class, obj.getKey()); + assertEquals(loaded.getKey(), obj.getKey()); + assertTrue(Arrays.equals(loaded.getBinaryAttribute(), (generateByteArray(contentLength)))); + assertEquals(1, loaded.getBinarySetAttribute().size()); + assertTrue(setContainsBytes(loaded.getBinarySetAttribute(), generateByteArray(contentLength))); + } + } + + /** + * Tests saving an incomplete object into DynamoDB + */ + @Test + public void testIncompleteObject() { + // test BinaryAttributeClass + BinaryAttributeByteBufferClass byteBufferObj = getUniqueByteBufferObject(contentLength); + byteBufferObj.setBinarySetAttribute(null); + DynamoDBMapper util = new DynamoDBMapper(dynamo); + util.save(byteBufferObj); + + BinaryAttributeByteBufferClass loadedX = util.load(BinaryAttributeByteBufferClass.class, byteBufferObj.getKey()); + assertEquals(loadedX.getKey(), byteBufferObj.getKey()); + assertEquals(loadedX.getBinaryAttribute(), ByteBuffer.wrap(generateByteArray(contentLength))); + assertEquals(loadedX.getBinarySetAttribute(), null); + + + // test removing an attribute + assertNotNull(byteBufferObj.getBinaryAttribute()); + byteBufferObj.setBinaryAttribute(null); + util.save(byteBufferObj); + + loadedX = util.load(BinaryAttributeByteBufferClass.class, byteBufferObj.getKey()); + assertEquals(loadedX.getKey(), byteBufferObj.getKey()); + assertEquals(loadedX.getBinaryAttribute(), null); + assertEquals(loadedX.getBinarySetAttribute(), null); + + // test BinaryAttributeByteArrayClass + BinaryAttributeByteArrayClass bytesObj = getUniqueBytesObject(contentLength); + bytesObj.setBinarySetAttribute(null); + util.save(bytesObj); + + BinaryAttributeByteArrayClass loadedY = util.load(BinaryAttributeByteArrayClass.class, bytesObj.getKey()); + assertEquals(loadedY.getKey(), bytesObj.getKey()); + assertTrue(Arrays.equals(loadedY.getBinaryAttribute(), generateByteArray(contentLength))); + assertEquals(loadedY.getBinarySetAttribute(), null); + + + // test removing an attribute + assertNotNull(bytesObj.getBinaryAttribute()); + bytesObj.setBinaryAttribute(null); + util.save(bytesObj); + + loadedY = util.load(BinaryAttributeByteArrayClass.class, bytesObj.getKey()); + assertEquals(loadedY.getKey(), bytesObj.getKey()); + assertEquals(loadedY.getBinaryAttribute(), null); + assertEquals(loadedY.getBinarySetAttribute(), null); + } + + @Test + public void testUpdate() { + // test BinaryAttributeClass + List byteBufferObjs = new ArrayList(); + for ( int i = 0; i < 5; i++ ) { + BinaryAttributeByteBufferClass obj = getUniqueByteBufferObject(contentLength); + byteBufferObjs.add(obj); + } + + DynamoDBMapper util = new DynamoDBMapper(dynamo); + for (BinaryAttributeByteBufferClass obj : byteBufferObjs) { + util.save(obj); + } + + for ( BinaryAttributeByteBufferClass obj : byteBufferObjs ) { + BinaryAttributeByteBufferClass replacement = getUniqueByteBufferObject(contentLength - 1); + replacement.setKey(obj.getKey()); + util.save(replacement); + + BinaryAttributeByteBufferClass loaded = util.load(BinaryAttributeByteBufferClass.class, obj.getKey()); + assertEquals(loaded.getKey(), obj.getKey()); + assertEquals(loaded.getBinaryAttribute(), ByteBuffer.wrap(generateByteArray(contentLength - 1))); + assertTrue(loaded.getBinarySetAttribute().contains(ByteBuffer.wrap(generateByteArray(contentLength - 1)))); + + } + + // test BinaryAttributeByteArrayClass + List bytesObj = new ArrayList(); + for ( int i = 0; i < 5; i++ ) { + BinaryAttributeByteArrayClass obj = getUniqueBytesObject(contentLength); + bytesObj.add(obj); + } + + for (BinaryAttributeByteArrayClass obj : bytesObj) { + util.save(obj); + } + + for ( BinaryAttributeByteArrayClass obj : bytesObj ) { + BinaryAttributeByteArrayClass replacement = getUniqueBytesObject(contentLength - 1); + replacement.setKey(obj.getKey()); + util.save(replacement); + + BinaryAttributeByteArrayClass loaded = util.load(BinaryAttributeByteArrayClass.class, obj.getKey()); + assertEquals(loaded.getKey(), obj.getKey()); + assertTrue(Arrays.equals(loaded.getBinaryAttribute(), (generateByteArray(contentLength - 1)))); + assertEquals(1, loaded.getBinarySetAttribute().size()); + assertTrue(setContainsBytes(loaded.getBinarySetAttribute(), generateByteArray(contentLength - 1))); + + } + } + + @Test + public void testDelete() throws Exception { + // test BinaryAttributeClass + BinaryAttributeByteBufferClass byteBufferObj = getUniqueByteBufferObject(contentLength); + DynamoDBMapper util = new DynamoDBMapper(dynamo); + util.save(byteBufferObj); + + util.delete(byteBufferObj); + assertNull(util.load(BinaryAttributeByteBufferClass.class, byteBufferObj.getKey())); + + // test BinaryAttributeByteArrayClass + BinaryAttributeByteArrayClass bytesObj = getUniqueBytesObject(contentLength); + util.save(bytesObj); + + util.delete(bytesObj); + assertNull(util.load(BinaryAttributeByteArrayClass.class, bytesObj.getKey())); + + } + + private BinaryAttributeByteArrayClass getUniqueBytesObject(int contentLength) { + BinaryAttributeByteArrayClass obj = new BinaryAttributeByteArrayClass(); + obj.setKey(String.valueOf(startKey++)); + obj.setBinaryAttribute(generateByteArray(contentLength)); + Set byteArray = new HashSet(); + byteArray.add(generateByteArray(contentLength)); + obj.setBinarySetAttribute(byteArray); + return obj; + } + + private boolean setContainsBytes(Set set, byte[] bytes) { + Iterator iter = set.iterator(); + while (iter.hasNext()) { + if (Arrays.equals(iter.next(), bytes)) + return true; + } + return false; + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/ComplexTypeIntegrationTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/ComplexTypeIntegrationTest.java new file mode 100644 index 000000000000..89cc48805114 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/ComplexTypeIntegrationTest.java @@ -0,0 +1,360 @@ +/* + * 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.mapper.dynamodb.mapper; + +import static org.junit.Assert.*; + +import java.io.StringReader; +import java.io.StringWriter; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.MappingJsonFactory; +import com.fasterxml.jackson.core.type.TypeReference; +import org.junit.Test; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperIntegrationTestBase; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTypeConverted; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTypeConvertedJson; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTypeConverter; + +/** + * Tests of the configuration object + */ +public class ComplexTypeIntegrationTest extends DynamoDBMapperIntegrationTestBase { + + // We don't start with the current system millis like other tests because + // it's out of the range of some data types + private static int start = 1; + private static int byteStart = -127; + + @Test + public void testComplexTypes() throws Exception { + DynamoDBMapper util = new DynamoDBMapper(dynamo); + + ComplexClass obj = getUniqueObject(); + util.save(obj); + ComplexClass loaded = util.load(ComplexClass.class, obj.getKey()); + assertEquals(obj, loaded); + } + + private ComplexClass getUniqueObject() { + ComplexClass obj = new ComplexClass(); + obj.setKey(String.valueOf(startKey++)); + obj.setBigDecimalAttribute(new BigDecimal(startKey++)); + obj.setBigIntegerAttribute(new BigInteger("" + startKey++)); + obj.setByteAttribute((byte) byteStart++); + obj.setByteObjectAttribute(new Byte("" + byteStart++)); + obj.setDoubleAttribute(new Double("" + start++)); + obj.setDoubleObjectAttribute(new Double("" + start++)); + obj.setFloatAttribute(new Float("" + start++)); + obj.setFloatObjectAttribute(new Float("" + start++)); + obj.setIntAttribute(new Integer("" + start++)); + obj.setIntegerAttribute(new Integer("" + start++)); + obj.setLongAttribute(new Long("" + start++)); + obj.setLongObjectAttribute(new Long("" + start++)); + obj.setDateAttribute(new Date(startKey++)); + obj.setBooleanAttribute(start++ % 2 == 0); + obj.setBooleanObjectAttribute(start++ % 2 == 0); + obj.setExtraField("" + startKey++); + Calendar cal = GregorianCalendar.getInstance(); + cal.setTime(new Date(startKey++)); + obj.setCalendarAttribute(cal); + obj.setComplexNestedType(new ComplexNestedType("" + start++, start++, new ComplexNestedType("" + start++, + start++, null))); + List complexTypes = new ArrayList(); + complexTypes.add(new ComplexNestedType("" + start++, start++, + new ComplexNestedType("" + start++, start++, null))); + complexTypes.add(new ComplexNestedType("" + start++, start++, new ComplexNestedType("" + start++, start++, null))); + complexTypes.add(new ComplexNestedType("" + start++, start++, new ComplexNestedType("" + start++, start++, null))); + obj.setComplexNestedTypeList(complexTypes); + return obj; + } + + public static final class ComplexNestedListTypeMarshaller implements DynamoDBTypeConverter> { + @Override + public String convert(final List object) { + try { + StringWriter writer = new StringWriter(); + JsonFactory jsonFactory = new MappingJsonFactory(); + JsonGenerator jsonGenerator = jsonFactory.createJsonGenerator(writer); + jsonGenerator.writeObject(object); + return writer.toString(); + } catch ( Exception e ) { + throw new RuntimeException(e); + } + } + @Override + public List unconvert(String obj) { + try { + JsonFactory jsonFactory = new MappingJsonFactory(); + JsonParser jsonParser = jsonFactory.createJsonParser(new StringReader(obj)); + return jsonParser.readValueAs(new TypeReference>() { + }); + } catch ( Exception e ) { + throw new RuntimeException(e); + } + } + } + + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static final class ComplexClass extends NumberAttributeClass { + + private String extraField; + private ComplexNestedType complexNestedType; + private List complexNestedTypeList; + + @DynamoDBTypeConvertedJson + public ComplexNestedType getComplexNestedType() { + return complexNestedType; + } + + public void setComplexNestedType(ComplexNestedType complexNestedType) { + this.complexNestedType = complexNestedType; + } + + @DynamoDBTypeConverted(converter=ComplexNestedListTypeMarshaller.class) + public List getComplexNestedTypeList() { + return complexNestedTypeList; + } + + public void setComplexNestedTypeList(List complexNestedTypeList) { + this.complexNestedTypeList = complexNestedTypeList; + } + + public String getExtraField() { + return extraField; + } + + public void setExtraField(String extraField) { + this.extraField = extraField; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((complexNestedType == null) ? 0 : complexNestedType.hashCode()); + result = prime * result + ((complexNestedTypeList == null) ? 0 : complexNestedTypeList.hashCode()); + result = prime * result + ((extraField == null) ? 0 : extraField.hashCode()); + return result; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + if ( this == obj ) + return true; + if ( !super.equals(obj) ) + return false; + if ( getClass() != obj.getClass() ) + return false; + ComplexClass other = (ComplexClass) obj; + if ( complexNestedType == null ) { + if ( other.complexNestedType != null ) + return false; + } else if ( !complexNestedType.equals(other.complexNestedType) ) + return false; + if ( complexNestedTypeList == null ) { + if ( other.complexNestedTypeList != null ) + return false; + } else if ( !complexNestedTypeList.equals(other.complexNestedTypeList) ) + return false; + if ( extraField == null ) { + if ( other.extraField != null ) + return false; + } else if ( !extraField.equals(other.extraField) ) + return false; + return true; + } + + } + + public static final class ComplexNestedType { + + private String stringValue; + private Integer intValue; + private ComplexNestedType nestedType; + + public ComplexNestedType() { + } + + public ComplexNestedType(String stringValue, Integer intValue, ComplexNestedType nestedType) { + super(); + this.stringValue = stringValue; + this.intValue = intValue; + this.nestedType = nestedType; + } + + public String getStringValue() { + return stringValue; + } + + public void setStringValue(String stringValue) { + this.stringValue = stringValue; + } + + public Integer getIntValue() { + return intValue; + } + + public void setIntValue(Integer intValue) { + this.intValue = intValue; + } + + public ComplexNestedType getNestedType() { + return nestedType; + } + + public void setNestedType(ComplexNestedType nestedType) { + this.nestedType = nestedType; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((intValue == null) ? 0 : intValue.hashCode()); + result = prime * result + ((nestedType == null) ? 0 : nestedType.hashCode()); + result = prime * result + ((stringValue == null) ? 0 : stringValue.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) + return true; + if ( obj == null ) + return false; + if ( getClass() != obj.getClass() ) + return false; + ComplexNestedType other = (ComplexNestedType) obj; + if ( intValue == null ) { + if ( other.intValue != null ) + return false; + } else if ( !intValue.equals(other.intValue) ) + return false; + if ( nestedType == null ) { + if ( other.nestedType != null ) + return false; + } else if ( !nestedType.equals(other.nestedType) ) + return false; + if ( stringValue == null ) { + if ( other.stringValue != null ) + return false; + } else if ( !stringValue.equals(other.stringValue) ) + return false; + return true; + } + + } + + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static final class ComplexKey { + + private ComplexNestedType key; + private String otherAttribute; + + @DynamoDBHashKey + @DynamoDBTypeConvertedJson + public ComplexNestedType getKey() { + return key; + } + + public void setKey(ComplexNestedType key) { + this.key = key; + } + + public String getOtherAttribute() { + return otherAttribute; + } + + public void setOtherAttribute(String otherAttribute) { + this.otherAttribute = otherAttribute; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((key == null) ? 0 : key.hashCode()); + result = prime * result + ((otherAttribute == null) ? 0 : otherAttribute.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) + return true; + if ( obj == null ) + return false; + if ( getClass() != obj.getClass() ) + return false; + ComplexKey other = (ComplexKey) obj; + if ( key == null ) { + if ( other.key != null ) + return false; + } else if ( !key.equals(other.key) ) + return false; + if ( otherAttribute == null ) { + if ( other.otherAttribute != null ) + return false; + } else if ( !otherAttribute.equals(other.otherAttribute) ) + return false; + return true; + } + } + + /** + * Tests using a complex type for a (string) key + */ + @Test + public void testComplexKey() throws Exception { + ComplexKey obj = new ComplexKey(); + ComplexNestedType key = new ComplexNestedType(); + key.setIntValue(start++); + key.setStringValue("" + start++); + obj.setKey(key); + obj.setOtherAttribute("" + start++); + + DynamoDBMapper mapper = new DynamoDBMapper(dynamo); + + mapper.save(obj); + ComplexKey loaded = mapper.load(ComplexKey.class, obj.getKey()); + assertEquals(obj, loaded); + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/ConfigurationIntegrationTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/ConfigurationIntegrationTest.java new file mode 100644 index 000000000000..d4eac7f3e1d3 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/ConfigurationIntegrationTest.java @@ -0,0 +1,323 @@ +/* + * 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.mapper.dynamodb.mapper; + +import static org.junit.Assert.*; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.UUID; + +import org.junit.Test; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperIntegrationTestBase; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAutoGeneratedKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.SaveBehavior; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.TableNameOverride; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.TableNameResolver; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.ObjectTableNameResolver; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; + +/** + * Tests of the configuration object + */ +public class ConfigurationIntegrationTest extends DynamoDBMapperIntegrationTestBase { + + // We don't start with the current system millis like other tests because + // it's out of the range of some data types + private static int start = 1; + private static int byteStart = -127; + + @Test + public void testClobber() throws Exception { + DynamoDBMapper util = new DynamoDBMapper(dynamo, new DynamoDBMapperConfig(SaveBehavior.CLOBBER)); + + NumberAttributeClassExtended obj = getUniqueObject(); + util.save(obj); + assertEquals(obj, util.load(obj.getClass(), obj.getKey())); + + NumberAttributeClass copy = copy(obj); + util.save(copy); + assertEquals(copy, util.load(copy.getClass(), obj.getKey())); + + // We should have lost the extra field because of the clobber behavior + assertNull(util.load(NumberAttributeClassExtended.class, obj.getKey()).getExtraField()); + + // Now test overriding the clobber behavior on a per-save basis + obj = getUniqueObject(); + util.save(obj); + assertEquals(obj, util.load(obj.getClass(), obj.getKey())); + + copy = copy(obj); + util.save(copy, new DynamoDBMapperConfig(SaveBehavior.UPDATE)); + assertEquals(copy, util.load(copy.getClass(), obj.getKey())); + + // We shouldn't have lost any extra info + assertNotNull(util.load(NumberAttributeClassExtended.class, obj.getKey()).getExtraField()); + } + + @Test + public void testTableOverride() throws Exception { + DynamoDBMapper util = new DynamoDBMapper(dynamo); + + TableOverrideTestClass obj = new TableOverrideTestClass(); + obj.setOtherField(UUID.randomUUID().toString()); + + try { + util.save(obj); + fail("Expected an exception"); + } catch ( Exception e ) { + } + + util.save(obj, new DynamoDBMapperConfig(new TableNameOverride("aws-java-sdk-util"))); + + try { + util.load(TableOverrideTestClass.class, obj.getKey()); + fail("Expected an exception"); + } catch ( Exception e ) { + } + + Object loaded = util.load(TableOverrideTestClass.class, obj.getKey(), new DynamoDBMapperConfig(TableNameOverride.withTableNamePrefix("aws-"))); + assertEquals(loaded, obj); + + try { + util.delete(obj); + fail("Expected an exception"); + } catch ( Exception e ) { + } + + util.delete(obj, new DynamoDBMapperConfig(TableNameOverride.withTableNamePrefix("aws-"))); + } + + @Test + public void testTableNameResolver() { + final String REAL_TABLE_NAME = "aws-java-sdk-util"; + + DynamoDBMapper mapper = new DynamoDBMapper(dynamo); + + final TableOverrideTestClass obj = new TableOverrideTestClass(); + obj.setOtherField(UUID.randomUUID().toString()); + + try { + mapper.save(obj); + fail("Expected an exception, because the POJO is annotated with a non-existent table."); + } catch ( Exception e ) { + } + + // Use TableNameResolver to save to the real table + mapper.save(obj, new DynamoDBMapperConfig(new TableNameResolver() { + @Override + public String getTableName(Class clazz, DynamoDBMapperConfig config) { + if (clazz.equals(TableOverrideTestClass.class)) { + return REAL_TABLE_NAME; + } + throw new RuntimeException("Unexpected data object type."); + } + })); + + try { + mapper.load(TableOverrideTestClass.class, obj.getKey()); + fail("Expected an exception, because the POJO is annotated with a non-existent table."); + } catch ( Exception e ) { + } + + // Use ObjectTableNameResolver to load from the real table + Object loaded = mapper.load(obj, + new DynamoDBMapperConfig(new ObjectTableNameResolver() { + @Override + public String getTableName(Object objectToLoad, DynamoDBMapperConfig config) { + if (objectToLoad == obj) { + return REAL_TABLE_NAME; + } + throw new RuntimeException("Unexpected data object."); + } + })); + assertEquals(loaded, obj); + + try { + mapper.delete(obj); + fail("Expected an exception, because the POJO is annotated with a non-existent table."); + } catch ( Exception e ) { + } + + // When used at the same time, ObjectTableNameResolver should have the highest priority + + final String NON_EXISTENT_TABLE_NAME = UUID.randomUUID().toString(); + mapper.delete(obj, new DynamoDBMapperConfig.Builder() + .withTableNameOverride(new TableNameOverride(NON_EXISTENT_TABLE_NAME)) + .withTableNameResolver(new TableNameResolver() { + @Override + public String getTableName(Class clazz, DynamoDBMapperConfig config) { + return NON_EXISTENT_TABLE_NAME; + } + }) + .withObjectTableNameResolver(new ObjectTableNameResolver() { + @Override + public String getTableName(Object object, DynamoDBMapperConfig config) { + return REAL_TABLE_NAME; + } + }).build()); + } + + private NumberAttributeClassExtended getUniqueObject() { + NumberAttributeClassExtended obj = new NumberAttributeClassExtended(); + obj.setKey(String.valueOf(startKey++)); + obj.setBigDecimalAttribute(new BigDecimal(startKey++)); + obj.setBigIntegerAttribute(new BigInteger("" + startKey++)); + obj.setByteAttribute((byte) byteStart++); + obj.setByteObjectAttribute(new Byte("" + byteStart++)); + obj.setDoubleAttribute(new Double("" + start++)); + obj.setDoubleObjectAttribute(new Double("" + start++)); + obj.setFloatAttribute(new Float("" + start++)); + obj.setFloatObjectAttribute(new Float("" + start++)); + obj.setIntAttribute(new Integer("" + start++)); + obj.setIntegerAttribute(new Integer("" + start++)); + obj.setLongAttribute(new Long("" + start++)); + obj.setLongObjectAttribute(new Long("" + start++)); + obj.setDateAttribute(new Date(startKey++)); + obj.setBooleanAttribute(start++ % 2 == 0); + obj.setBooleanObjectAttribute(start++ % 2 == 0); + obj.setExtraField("" + startKey++); + Calendar cal = GregorianCalendar.getInstance(); + cal.setTime(new Date(startKey++)); + obj.setCalendarAttribute(cal); + return obj; + } + + private NumberAttributeClass copy(NumberAttributeClassExtended obj) { + NumberAttributeClass copy = new NumberAttributeClass(); + copy.setKey(obj.getKey()); + copy.setBigDecimalAttribute(obj.getBigDecimalAttribute()); + copy.setBigIntegerAttribute(obj.getBigIntegerAttribute()); + copy.setByteAttribute(obj.getByteAttribute()); + copy.setByteObjectAttribute(obj.getByteObjectAttribute()); + copy.setDoubleAttribute(obj.getDoubleAttribute()); + copy.setDoubleObjectAttribute(obj.getDoubleObjectAttribute()); + copy.setFloatAttribute(obj.getFloatAttribute()); + copy.setFloatObjectAttribute(obj.getFloatObjectAttribute()); + copy.setIntAttribute(obj.getIntAttribute()); + copy.setIntegerAttribute(obj.getIntegerAttribute()); + copy.setLongAttribute(obj.getLongAttribute()); + copy.setLongObjectAttribute(obj.getLongObjectAttribute()); + copy.setDateAttribute(obj.getDateAttribute()); + copy.setBooleanAttribute(obj.isBooleanAttribute()); + copy.setBooleanObjectAttribute(obj.getBooleanObjectAttribute()); + return copy; + } + + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static final class NumberAttributeClassExtended extends NumberAttributeClass { + + private String extraField; + + public String getExtraField() { + return extraField; + } + + public void setExtraField(String extraField) { + this.extraField = extraField; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((extraField == null) ? 0 : extraField.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) + return true; + if ( !super.equals(obj) ) + return false; + if ( getClass() != obj.getClass() ) + return false; + NumberAttributeClassExtended other = (NumberAttributeClassExtended) obj; + if ( extraField == null ) { + if ( other.extraField != null ) + return false; + } else if ( !extraField.equals(other.extraField) ) + return false; + return true; + } + } + + @DynamoDBTable(tableName = "java-sdk-util") // doesn't exist + public static final class TableOverrideTestClass { + + private String key; + private String otherField; + + @DynamoDBAutoGeneratedKey + @DynamoDBHashKey + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getOtherField() { + return otherField; + } + + public void setOtherField(String otherField) { + this.otherField = otherField; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((key == null) ? 0 : key.hashCode()); + result = prime * result + ((otherField == null) ? 0 : otherField.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) + return true; + if ( obj == null ) + return false; + if ( getClass() != obj.getClass() ) + return false; + TableOverrideTestClass other = (TableOverrideTestClass) obj; + if ( key == null ) { + if ( other.key != null ) + return false; + } else if ( !key.equals(other.key) ) + return false; + if ( otherField == null ) { + if ( other.otherField != null ) + return false; + } else if ( !otherField.equals(other.otherField) ) + return false; + return true; + } + + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/DelimitedIntegrationTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/DelimitedIntegrationTest.java new file mode 100644 index 000000000000..f3c88952ad9f --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/DelimitedIntegrationTest.java @@ -0,0 +1,464 @@ +/* + * 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.mapper.dynamodb.mapper; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAutoGeneratedKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAutoGeneratedTimestamp; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMappingException; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBDelimited; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTypeConvertedTimestamp; +import com.amazonaws.services.dynamodbv2.model.AttributeValue; +import software.amazon.awssdk.mapper.dynamodb.pojos.AutoKeyAndVal; +import software.amazon.awssdk.mapper.dynamodb.pojos.Currency; +import software.amazon.awssdk.mapper.dynamodb.pojos.DateRange; +import software.amazon.awssdk.mapper.dynamodb.pojos.KeyAndVal; +import software.amazon.awssdk.mapper.dynamodb.pojos.PhoneNumber; + +import java.text.SimpleDateFormat; +import java.util.Collections; +import java.util.Date; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import org.junit.Test; + +/** + * Tests updating component attribute fields correctly. + */ +public class DelimitedIntegrationTest extends AbstractKeyAndValIntegrationTestCase { + + /** + * Test using {@code PhoneNumber}. + */ + @Test + public void testPhoneNumber() { + final KeyAndPhoneNumber object = new KeyAndPhoneNumber(); + object.setVal(new PhoneNumber("206","266","1000")); + assertBeforeAndAfterChange(false, object); + } + + /** + * Test using {@code PhoneNumber}. + */ + @Test + public void testPhoneNumberAreaCodeNull() { + final KeyAndPhoneNumber object = new KeyAndPhoneNumber(); + object.setVal(new PhoneNumber(null,"266","1000")); + assertBeforeAndAfterChange(false, object); + } + + /** + * Test using {@code PhoneNumber}. + */ + @Test + public void testPhoneNumberAreaCodeEmpty() { + final KeyAndPhoneNumber object = new KeyAndPhoneNumber(); + object.setVal(new PhoneNumber("","266","1000")); + + final PhoneNumber after = assertBeforeAndAfterChange(null, object); + assertNotNull(after); + assertNull(after.getAreaCode()); + assertEquals(object.getVal().getExchange(), after.getExchange()); + assertEquals(object.getVal().getSubscriber(), after.getSubscriber()); + } + + /** + * Test using {@code PhoneNumber}. + */ + @Test + public void testPhoneNumberNull() { + final KeyAndPhoneNumber object = new KeyAndPhoneNumber(); + object.setVal(null); + assertBeforeAndAfterChange(false, object); + } + + /** + * Test using {@code PhoneNumber}. + */ + @Test + public void testPhoneNumberAllNull() { + final KeyAndPhoneNumber object = new KeyAndPhoneNumber(); + object.setVal(new PhoneNumber(null,null,null)); + + final PhoneNumber after = assertBeforeAndAfterChange(null, object); + assertNull(after); //<- and empty object produces a null val + } + + /** + * Test using {@code PhoneNumber}. + */ + @Test + public void testKeyAndSpecialCharacterDelimitedierPhoneNumber() { + final KeyAndSpecialCharacterDelimitedierPhoneNumber object = new KeyAndSpecialCharacterDelimitedierPhoneNumber(); + object.setVal(new PhoneNumber("206","867","5309")); + assertBeforeAndAfterChange(false, object); + } + + /** + * Test using {@code PhoneNumber}. + */ + @Test + public void testKeyAndDefaultDelimitederPhoneNumber() { + final KeyAndDefaultDelimitederPhoneNumber object = new KeyAndDefaultDelimitederPhoneNumber(); + object.setVal(new PhoneNumber("206","867","5309")); + assertBeforeAndAfterChange(false, object); + } + + /** + * Test using {@code PhoneNumber}. + */ + @Test + public void testPhoneNumberDelimitedOnType() { + final KeyAndPhoneNumberDelimitedOnType object = new KeyAndPhoneNumberDelimitedOnType(); + object.setVal(new KeyAndPhoneNumberDelimitedOnType.Value()); + object.getVal().setAreaCode("206"); + object.getVal().setExchange("266"); + object.getVal().setSubscriber("1000"); + assertBeforeAndAfterChange(false, object); + } + + /** + * Test using {@code PhoneNumber}. + */ + @Test(expected=DynamoDBMappingException.class) + public void testKeyAndNoAttributeNamesPhoneNumber() { + final KeyAndNoAttributeNamesPhoneNumber object = new KeyAndNoAttributeNamesPhoneNumber(); + object.setVal(new PhoneNumber("206","867","5309")); + assertBeforeAndAfterChange(false, object); + } + + /** + * Test using {@code PhoneNumber}. + */ + @Test(expected=DynamoDBMappingException.class) + public void testExceptionOnPhoneNumberSet() { + final KeyAndPhoneNumberSet object = new KeyAndPhoneNumberSet(); + object.setVal(Collections.singleton(new PhoneNumber("206","266","1000"))); + assertBeforeAndAfterChange(false, object); + } + + /** + * Test using {@code DateRange}. + */ + @Test + public void testFormatDateRange() throws Exception { + final KeyAndFormatDateRange object = new KeyAndFormatDateRange(); + object.setVal(new KeyAndFormatDateRange.FormatDateRange()); + object.getVal().setStart(new Date(System.currentTimeMillis() - 6000L)); + object.getVal().setEnd(new Date(System.currentTimeMillis() + 6000L)); + assertBeforeAndAfterChange(false, object); + } + + /** + * Test using {@code DateRange}. + */ + @Test + public void testFormatDateRangeStartNull() throws Exception { + final KeyAndFormatDateRange object = new KeyAndFormatDateRange(); + object.setVal(new KeyAndFormatDateRange.FormatDateRange()); + object.getVal().setStart(null); + object.getVal().setEnd(new Date()); + assertBeforeAndAfterChange(false, object); + } + + /** + * Test using {@code DateRange}. + */ + @Test + public void testFormatDateRangeEndNull() throws Exception { + final KeyAndFormatDateRange object = new KeyAndFormatDateRange(); + object.setVal(new KeyAndFormatDateRange.FormatDateRange()); + object.getVal().setStart(new Date()); + object.getVal().setEnd(null); + assertBeforeAndAfterChange(false, object); + } + + /** + * Test using {@code DateRange}. + */ + @Test + public void testFormatDateRangeStartAndEndNull() { + final KeyAndFormatDateRange object = new KeyAndFormatDateRange(); + object.setVal(new KeyAndFormatDateRange.FormatDateRange()); + object.getVal().setStart(null); + object.getVal().setEnd(null); + + final KeyAndFormatDateRange.FormatDateRange after = assertBeforeAndAfterChange(null, object); + assertNull(after); //<- and empty object produces a null val + } + + /** + * Test using {@code DateRange}. + */ + @Test + public void testDelimitedKeyAndDate() { + final DelimitedKeyAndDate object = new DelimitedKeyAndDate(); + object.setKey(new DelimitedKeyAndDate.Key()); + object.getKey().setKey(UUID.randomUUID().toString()); + object.getKey().setVal(new Date()); + assertBeforeAndAfterChange(true, object); + } + + /** + * Test marshalling. + */ + @Test + public void testKeyAndCurrency() { + final KeyAndCurrency object = new KeyAndCurrency(); + object.setVal(new Currency(12.95D, "USD")); + assertBeforeAndAfterChange(false, object); + } + + /** + * Test marshalling. + */ + @Test(expected=DynamoDBMappingException.class) + public void testKeyAndNestedCurrency() { + final KeyAndNestedCurrency object = new KeyAndNestedCurrency(); + object.setVal(new KeyAndNestedCurrency.Value()); + object.getVal().setKey(UUID.randomUUID().toString()); + object.getVal().setVal(new Currency(12.95D, "USD")); + assertBeforeAndAfterChange(false, object); + } + + /** + * An object with {@code PhoneNumber}. + */ + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class KeyAndSpecialCharacterDelimitedierPhoneNumber extends AutoKeyAndVal { + @DynamoDBDelimited(attributeNames={"areaCode","exchange","subscriber"}, delimiter='\\') + public PhoneNumber getVal() { + return super.getVal(); + } + @Override + public void setVal(final PhoneNumber val) { + super.setVal(val); + } + } + + /** + * An object with {@code PhoneNumber}. + */ + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class KeyAndDefaultDelimitederPhoneNumber extends AutoKeyAndVal { + @DynamoDBDelimited(attributeNames={"areaCode","exchange","subscriber"}) + public PhoneNumber getVal() { + return super.getVal(); + } + @Override + public void setVal(final PhoneNumber val) { + super.setVal(val); + } + } + + /** + * An object with {@code PhoneNumber}. + */ + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class KeyAndPhoneNumber extends AutoKeyAndVal { + @DynamoDBDelimited(attributeNames={"areaCode","exchange","subscriber"}, delimiter='-') + public PhoneNumber getVal() { + return super.getVal(); + } + @Override + public void setVal(final PhoneNumber val) { + super.setVal(val); + } + } + + /** + * An object with {@code PhoneNumber}. + */ + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class KeyAndPhoneNumberDelimitedOnType extends AutoKeyAndVal { + public Value getVal() { + return super.getVal(); + } + @Override + public void setVal(final Value val) { + super.setVal(val); + } + @DynamoDBDelimited(attributeNames={"areaCode","exchange","subscriber"}, delimiter='.') + public static class Value extends PhoneNumber {} + } + + /** + * An object with {@code PhoneNumber}. + */ + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class KeyAndNoAttributeNamesPhoneNumber extends AutoKeyAndVal { + @DynamoDBDelimited(attributeNames={}) + public PhoneNumber getVal() { + return super.getVal(); + } + @Override + public void setVal(final PhoneNumber val) { + super.setVal(val); + } + } + + /** + * An object with {@code PhoneNumber}. + */ + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class KeyAndPhoneNumberSet extends AutoKeyAndVal> { + @DynamoDBDelimited(attributeNames={"areaCode","exchange","subscriber"}, delimiter='-') + public Set getVal() { + return super.getVal(); + } + @Override + public void setVal(final Set val) { + super.setVal(val); + } + } + + /** + * An object with {@code DateRange}. + */ + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class KeyAndDateRange extends AutoKeyAndVal { + @DynamoDBDelimited(attributeNames={"start","end"}) + public DateRange getVal() { + return super.getVal(); + } + @Override + public void setVal(final DateRange val) { + super.setVal(val); + } + } + + /** + * An object with {@code DateRange}. + */ + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class KeyAndFormatDateRange extends AutoKeyAndVal { + @DynamoDBDelimited(attributeNames={"start","end"}) + public FormatDateRange getVal() { + return super.getVal(); + } + @Override + public void setVal(final FormatDateRange val) { + super.setVal(val); + } + public static class FormatDateRange extends DateRange { + @DynamoDBTypeConvertedTimestamp(pattern="yyyyMMddHHmmssSSS") + public Date getStart() { + return super.getStart(); + } + @DynamoDBTypeConvertedTimestamp(pattern="yyyyMMddHHmmssSSS") + public Date getEnd() { + return super.getEnd(); + } + } + } + + /** + * An object with {@code DateRange}. + */ + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class DelimitedKeyAndDate extends KeyAndVal { + @DynamoDBHashKey @DynamoDBDelimited(attributeNames={"key","val"}) + public DelimitedKeyAndDate.Key getKey() { + return super.getKey(); + } + @Override + public void setKey(final DelimitedKeyAndDate.Key key) { + super.setKey(key); + } + @DynamoDBAutoGeneratedTimestamp + public Date getVal() { + return super.getVal(); + } + @Override + public void setVal(final Date val) { + super.setVal(val); + } + public static class Key extends KeyAndVal { + @Override + public String getKey() { + return super.getKey(); + } + @Override + public void setKey(final String key) { + super.setKey(key); + } + @DynamoDBTypeConvertedTimestamp(pattern="yyyyMMdd") + public Date getVal() { + return super.getVal(); + } + @Override + public void setVal(final Date val) { + super.setVal(val); + } + } + } + + /** + * An object with a complex type. + */ + @DynamoDBTable(tableName="aws-java-sdk-util") + public static class KeyAndCurrency extends AutoKeyAndVal { + @DynamoDBDelimited(attributeNames={"amount","unit"}, delimiter='$') + public Currency getVal() { + return super.getVal(); + } + @Override + public void setVal(final Currency val) { + super.setVal(val); + } + } + + /** + * An object with a complex type. + */ + @DynamoDBTable(tableName="aws-java-sdk-util") + public static class KeyAndNestedCurrency extends AutoKeyAndVal { + @DynamoDBDelimited(attributeNames={"key","val"}, delimiter='#') + public KeyAndNestedCurrency.Value getVal() { + return super.getVal(); + } + @Override + public void setVal(final KeyAndNestedCurrency.Value val) { + super.setVal(val); + } + public static class Value extends KeyAndVal { + @Override + public String getKey() { + return super.getKey(); + } + @Override + public void setKey(final String key) { + super.setKey(key); + } + @Override + public Currency getVal() { + return super.getVal(); + } + @Override + public void setVal(final Currency val) { + super.setVal(val); + } + } + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/DynamoDBTypeConvertedEpochDateTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/DynamoDBTypeConvertedEpochDateTest.java new file mode 100644 index 000000000000..738ac69ccdd6 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/DynamoDBTypeConvertedEpochDateTest.java @@ -0,0 +1,213 @@ +/* + * 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.mapper.dynamodb.mapper; + +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTypeConvertedEpochDate; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; +import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; +import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest; +import software.amazon.awssdk.services.dynamodb.model.UpdateItemResponse; + +import java.util.HashMap; +import java.util.Map; + +import org.joda.time.DateTime; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Calendar; +import java.util.Date; +import java.util.UUID; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Tests to ensure {@link Date}, {@link Calendar}, and {@link DateTime} objects can be coerced to a numeric Dynamo DB attribute + * via the {@link DynamoDBTypeConvertedEpochDate} annontation. + */ +@RunWith(MockitoJUnitRunner.class) +public class DynamoDBTypeConvertedEpochDateTest { + + private static final String HASH_KEY = "1234"; + + private DynamoDBMapper mapper; + + @Mock + private DynamoDbClient ddb; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mapper = new DynamoDBMapper(ddb); + // Just stub dummy response for all save related tests + when(ddb.updateItem(any(UpdateItemRequest.class))).thenReturn(UpdateItemResponse.builder().build()); + } + + @Test + public void saveItem_DateSentAsNumericUpdate() { + Date date = new Date(); + mapper.save(new PojoWithDate() + .setHashKey(UUID.randomUUID().toString()) + .setDate(date)); + verifyAttributeUpdatedWithValue("date", AttributeValue.builder().n(String.valueOf(date.getTime())).build()); + } + + @Test + public void saveItem_CalendarSentAsNumericUpdate() { + Calendar calendar = Calendar.getInstance(); + mapper.save(new PojoWithDate() + .setHashKey(UUID.randomUUID().toString()) + .setCalendar(calendar)); + verifyAttributeUpdatedWithValue("calendar", AttributeValue.builder().n(String.valueOf(calendar.getTime().getTime())).build()); + } + + @Test + public void saveItem_DateTimeSentAsNumericUpdate() { + DateTime dateTime = new DateTime(); + mapper.save(new PojoWithDate() + .setHashKey(UUID.randomUUID().toString()) + .setDateTime(dateTime)); + verifyAttributeUpdatedWithValue("dateTime", AttributeValue.builder().n(String.valueOf(dateTime.toDate().getTime())).build()); + } + + @Test + public void getItem_WithNumericDateInResponse_UnmarshalledCorrectly() { + stubGetItemRequest("date", AttributeValue.builder().n("1234").build()); + final PojoWithDate pojo = loadPojo(); + assertThat(pojo.getDate().getTime(), equalTo(1234L)); + } + + @Test + public void getItem_WithNumericCalendarInResponse_UnmarshalledCorrectly() { + stubGetItemRequest("calendar", AttributeValue.builder().n("1234").build()); + final PojoWithDate pojo = loadPojo(); + assertThat(pojo.getCalendar().getTime().getTime(), equalTo(1234L)); + } + + @Test + public void getItem_WithNumericDateTimeInResponse_UnmarshalledCorrectly() { + stubGetItemRequest("dateTime", AttributeValue.builder().n("1234").build()); + final PojoWithDate pojo = loadPojo(); + assertThat(pojo.getDateTime().toDate().getTime(), equalTo(1234L)); + } + + private PojoWithDate loadPojo() { + return mapper.load(new PojoWithDate().setHashKey(HASH_KEY)); + } + + /** + * Stub a call to getItem to return a result with the given attribute value in the item. + * + * @param attributeName Attribute name to return in result (in addition to hash key) + * @param attributeValue Attribute value to return in result (in addition to hash key) + */ + private void stubGetItemRequest(String attributeName, AttributeValue attributeValue) { + when(ddb.getItem(any(GetItemRequest.class))).thenReturn(createGetItemResult(attributeName, attributeValue)); + } + + /** + * Create a {@link GetItemResponse} with the hash key value ({@value #HASH_KEY} and the additional attribute. + * + * @param attributeName Additional attribute to include in created {@link GetItemResponse}. + * @param attributeValue Value of additional attribute. + */ + private GetItemResponse createGetItemResult(String attributeName, AttributeValue attributeValue) { + Map item = new HashMap<>(); + item.put("hashKey", AttributeValue.builder().s(HASH_KEY).build()); + item.put(attributeName, attributeValue); + return GetItemResponse.builder().item(item).build(); + } + + /** + * Verifies the mapper results in an update item call that has an update for the appropriate attribute. + * + * @param attributeName Attribute expected to be updated. + * @param expected Expected value of update action. + */ + private void verifyAttributeUpdatedWithValue(String attributeName, AttributeValue expected) { + ArgumentCaptor updateItemRequestCaptor = ArgumentCaptor.forClass(UpdateItemRequest.class); + verify(ddb).updateItem(updateItemRequestCaptor.capture()); + assertEquals(expected, updateItemRequestCaptor.getValue().attributeUpdates().get(attributeName).value()); + } + + @DynamoDBTable(tableName = "PojoWithDate") + public static class PojoWithDate { + + @DynamoDBHashKey + private String hashKey; + + @DynamoDBTypeConvertedEpochDate + private Date date; + + @DynamoDBTypeConvertedEpochDate + private Calendar calendar; + + @DynamoDBTypeConvertedEpochDate + private DateTime dateTime; + + + public String getHashKey() { + return hashKey; + } + + public PojoWithDate setHashKey(String hashKey) { + this.hashKey = hashKey; + return this; + } + + public Date getDate() { + return date; + } + + public PojoWithDate setDate(Date date) { + this.date = date; + return this; + } + + public Calendar getCalendar() { + return calendar; + } + + public PojoWithDate setCalendar(Calendar calendar) { + this.calendar = calendar; + return this; + } + + public DateTime getDateTime() { + return dateTime; + } + + public PojoWithDate setDateTime(DateTime dateTime) { + this.dateTime = dateTime; + return this; + } + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/EmptyBinaryByteArrayAttributesTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/EmptyBinaryByteArrayAttributesTest.java new file mode 100644 index 000000000000..06115784a974 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/EmptyBinaryByteArrayAttributesTest.java @@ -0,0 +1,134 @@ +/* + * 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.mapper.dynamodb.mapper; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.HashMap; +import java.util.Map; + +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.AttributeValueUpdate; +import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; +import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; +import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; +import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest; +import software.amazon.awssdk.services.dynamodb.model.UpdateItemResponse; +import software.amazon.awssdk.mapper.dynamodb.pojos.BinaryAttributeByteArrayClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +/** + * Tests empty binary attributes represented as byte[] + */ +@RunWith(MockitoJUnitRunner.class) +public class EmptyBinaryByteArrayAttributesTest { + + private static final String BINARY_ATTRIBUTE = "binaryAttribute"; + private static final String KEY_NAME = "key"; + private static final String KEY_VALUE = "test-id"; + private static final byte[] EMPTY_BINARY = new byte[]{}; + private static final AttributeValue EMPTY_BINARY_AV = AttributeValue.builder().b(SdkBytes.fromByteArray(EMPTY_BINARY)).build(); + + private static final Map ITEM_MAP; + private static final Map KEY_MAP; + private static final BinaryAttributeByteArrayClass TEST_OBJECT; + + static { + KEY_MAP = new HashMap<>(); + KEY_MAP.put(KEY_NAME, AttributeValue.builder().s(KEY_VALUE).build()); + + ITEM_MAP = new HashMap<>(); + ITEM_MAP.put(KEY_NAME, AttributeValue.builder().s(KEY_VALUE).build()); + ITEM_MAP.put(BINARY_ATTRIBUTE, EMPTY_BINARY_AV); + + TEST_OBJECT = new BinaryAttributeByteArrayClass(); + TEST_OBJECT.setKey(KEY_VALUE); + TEST_OBJECT.setBinaryAttribute(EMPTY_BINARY); + } + + @Mock + private DynamoDbClient mockDynamo; + + @Captor + private ArgumentCaptor getItemRequestCaptor; + + @Captor + private ArgumentCaptor putItemRequestCaptor; + + @Captor + private ArgumentCaptor updateItemRequestArgumentCaptor; + + @Test + public void testLoad() { + when(mockDynamo.getItem(any(GetItemRequest.class))).thenReturn(GetItemResponse.builder().item(ITEM_MAP).build()); + DynamoDBMapper dynamoDBMapper = new DynamoDBMapper(mockDynamo); + + BinaryAttributeByteArrayClass x = dynamoDBMapper.load(BinaryAttributeByteArrayClass.class, + ITEM_MAP.get(KEY_NAME).s()); + assertEquals(ITEM_MAP.get(KEY_NAME).s(), x.getKey()); + // v2 SdkBytes.asByteArray() returns a defensive copy, so compare by content rather than identity. + assertArrayEquals(EMPTY_BINARY, x.getBinaryAttribute()); + + verify(mockDynamo).getItem(getItemRequestCaptor.capture()); + GetItemRequest getItemRequest = getItemRequestCaptor.getValue(); + assertEquals(KEY_MAP, getItemRequest.key()); + } + + @Test + public void testSaveUsingPut() { + DynamoDBMapper dynamoDBMapper = new DynamoDBMapper(mockDynamo); + + dynamoDBMapper.save(TEST_OBJECT, DynamoDBMapperConfig.builder() + .withSaveBehavior(DynamoDBMapperConfig.SaveBehavior.PUT) + .build()); + + verify(mockDynamo).putItem(putItemRequestCaptor.capture()); + PutItemRequest putItemRequest = putItemRequestCaptor.getValue(); + assertEquals(ITEM_MAP, putItemRequest.item()); + } + + @Test + public void testSaveUsingUpdate() { + when(mockDynamo.updateItem(any(UpdateItemRequest.class))) + .thenReturn(UpdateItemResponse.builder().attributes(ITEM_MAP).build()); + DynamoDBMapper dynamoDBMapper = new DynamoDBMapper(mockDynamo); + + dynamoDBMapper.save(TEST_OBJECT, DynamoDBMapperConfig.builder() + .withSaveBehavior(DynamoDBMapperConfig.SaveBehavior.UPDATE) + .build()); + + verify(mockDynamo).updateItem(updateItemRequestArgumentCaptor.capture()); + UpdateItemRequest updateItemRequest = updateItemRequestArgumentCaptor.getValue(); + assertEquals(KEY_MAP, updateItemRequest.key()); + Map updates = updateItemRequest.attributeUpdates(); + AttributeValueUpdate attributeValueUpdate = updates.get(BINARY_ATTRIBUTE); + assertEquals(EMPTY_BINARY_AV, attributeValueUpdate.value()); + assertEquals("PUT", attributeValueUpdate.actionAsString()); + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/EmptyBinaryByteBufferAttributesTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/EmptyBinaryByteBufferAttributesTest.java new file mode 100644 index 000000000000..d08296649306 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/EmptyBinaryByteBufferAttributesTest.java @@ -0,0 +1,133 @@ +/* + * 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.mapper.dynamodb.mapper; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Map; + +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.AttributeValueUpdate; +import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; +import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; +import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; +import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest; +import software.amazon.awssdk.services.dynamodb.model.UpdateItemResponse; +import software.amazon.awssdk.mapper.dynamodb.pojos.BinaryAttributeByteBufferClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +/** + * Tests empty binary attributes represented as ByteBuffer + */ +@RunWith(MockitoJUnitRunner.class) +public class EmptyBinaryByteBufferAttributesTest { + + private static final String BINARY_ATTRIBUTE = "binaryAttribute"; + private static final String KEY_NAME = "key"; + private static final String KEY_VALUE = "test-id"; + private static final ByteBuffer EMPTY_BINARY = ByteBuffer.wrap(new byte[]{}); + private static final AttributeValue EMPTY_BINARY_AV = AttributeValue.builder().b(SdkBytes.fromByteBuffer(EMPTY_BINARY)).build(); + + private static final Map ITEM_MAP; + private static final Map KEY_MAP; + private static final BinaryAttributeByteBufferClass TEST_OBJECT; + + static { + KEY_MAP = new HashMap<>(); + KEY_MAP.put(KEY_NAME, AttributeValue.builder().s(KEY_VALUE).build()); + + ITEM_MAP = new HashMap<>(); + ITEM_MAP.put(KEY_NAME, AttributeValue.builder().s(KEY_VALUE).build()); + ITEM_MAP.put(BINARY_ATTRIBUTE, EMPTY_BINARY_AV); + + TEST_OBJECT = new BinaryAttributeByteBufferClass(); + TEST_OBJECT.setKey(KEY_VALUE); + TEST_OBJECT.setBinaryAttribute(EMPTY_BINARY); + } + + @Mock + private DynamoDbClient mockDynamo; + + @Captor + private ArgumentCaptor getItemRequestCaptor; + + @Captor + private ArgumentCaptor putItemRequestCaptor; + + @Captor + private ArgumentCaptor updateItemRequestArgumentCaptor; + + @Test + public void testLoad() { + when(mockDynamo.getItem(any(GetItemRequest.class))).thenReturn(GetItemResponse.builder().item(ITEM_MAP).build()); + DynamoDBMapper dynamoDBMapper = new DynamoDBMapper(mockDynamo); + + BinaryAttributeByteBufferClass x = dynamoDBMapper.load(BinaryAttributeByteBufferClass.class, + ITEM_MAP.get(KEY_NAME).s()); + assertEquals(ITEM_MAP.get(KEY_NAME).s(), x.getKey()); + assertEquals(EMPTY_BINARY, x.getBinaryAttribute()); + + verify(mockDynamo).getItem(getItemRequestCaptor.capture()); + GetItemRequest getItemRequest = getItemRequestCaptor.getValue(); + assertEquals(KEY_MAP, getItemRequest.key()); + } + + @Test + public void testSaveUsingPut() { + DynamoDBMapper dynamoDBMapper = new DynamoDBMapper(mockDynamo); + + dynamoDBMapper.save(TEST_OBJECT, DynamoDBMapperConfig.builder() + .withSaveBehavior(DynamoDBMapperConfig.SaveBehavior.PUT) + .build()); + + verify(mockDynamo).putItem(putItemRequestCaptor.capture()); + PutItemRequest putItemRequest = putItemRequestCaptor.getValue(); + assertEquals(ITEM_MAP, putItemRequest.item()); + } + + @Test + public void testSaveUsingUpdate() { + when(mockDynamo.updateItem(any(UpdateItemRequest.class))) + .thenReturn(UpdateItemResponse.builder().attributes(ITEM_MAP).build()); + DynamoDBMapper dynamoDBMapper = new DynamoDBMapper(mockDynamo); + + dynamoDBMapper.save(TEST_OBJECT, DynamoDBMapperConfig.builder() + .withSaveBehavior(DynamoDBMapperConfig.SaveBehavior.UPDATE) + .build()); + + verify(mockDynamo).updateItem(updateItemRequestArgumentCaptor.capture()); + UpdateItemRequest updateItemRequest = updateItemRequestArgumentCaptor.getValue(); + assertEquals(KEY_MAP, updateItemRequest.key()); + Map updates = updateItemRequest.attributeUpdates(); + AttributeValueUpdate attributeValueUpdate = updates.get(BINARY_ATTRIBUTE); + assertEquals(EMPTY_BINARY_AV, attributeValueUpdate.value()); + assertEquals("PUT", attributeValueUpdate.actionAsString()); + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/EmptyBinarySetByteArrayAttributesTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/EmptyBinarySetByteArrayAttributesTest.java new file mode 100644 index 000000000000..090fa4eae08c --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/EmptyBinarySetByteArrayAttributesTest.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.mapper.dynamodb.mapper; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.AttributeValueUpdate; +import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; +import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; +import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; +import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest; +import software.amazon.awssdk.services.dynamodb.model.UpdateItemResponse; +import software.amazon.awssdk.mapper.dynamodb.pojos.BinaryAttributeByteArrayClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +/** + * Tests empty binary set attributes represented as byte[] + */ +@RunWith(MockitoJUnitRunner.class) +public class EmptyBinarySetByteArrayAttributesTest { + + private static final String BINARY_SET_ATTRIBUTE = "binarySetAttribute"; + private static final String KEY_NAME = "key"; + private static final String KEY_VALUE = "test-id"; + private static final byte[] EMPTY_BINARY = new byte[]{}; + private static final Set EMPTY_BINARY_SET; + private static final AttributeValue EMPTY_BINARY_SET_AV = AttributeValue.builder().bs(SdkBytes.fromByteArray(EMPTY_BINARY)).build(); + + private static final Map ITEM_MAP; + private static final Map KEY_MAP; + private static final BinaryAttributeByteArrayClass TEST_OBJECT; + + static { + EMPTY_BINARY_SET = new HashSet<>(); + EMPTY_BINARY_SET.add(EMPTY_BINARY); + + KEY_MAP = new HashMap<>(); + KEY_MAP.put(KEY_NAME, AttributeValue.builder().s(KEY_VALUE).build()); + + ITEM_MAP = new HashMap<>(); + ITEM_MAP.put(KEY_NAME, AttributeValue.builder().s(KEY_VALUE).build()); + ITEM_MAP.put(BINARY_SET_ATTRIBUTE, EMPTY_BINARY_SET_AV); + + TEST_OBJECT = new BinaryAttributeByteArrayClass(); + TEST_OBJECT.setKey(KEY_VALUE); + TEST_OBJECT.setBinarySetAttribute(EMPTY_BINARY_SET); + } + + @Mock + private DynamoDbClient mockDynamo; + + @Captor + private ArgumentCaptor getItemRequestCaptor; + + @Captor + private ArgumentCaptor putItemRequestCaptor; + + @Captor + private ArgumentCaptor updateItemRequestArgumentCaptor; + + @Test + public void testLoad() { + when(mockDynamo.getItem(any(GetItemRequest.class))).thenReturn(GetItemResponse.builder().item(ITEM_MAP).build()); + DynamoDBMapper dynamoDBMapper = new DynamoDBMapper(mockDynamo); + + BinaryAttributeByteArrayClass x = dynamoDBMapper.load(BinaryAttributeByteArrayClass.class, + ITEM_MAP.get(KEY_NAME).s()); + assertEquals(ITEM_MAP.get(KEY_NAME).s(), x.getKey()); + // v2 SdkBytes.asByteArray() returns a defensive copy, so Set equality (which compares byte[] + // elements by identity) would fail; compare the single element by content instead. + Set loadedSet = x.getBinarySetAttribute(); + assertEquals(EMPTY_BINARY_SET.size(), loadedSet.size()); + assertArrayEquals(EMPTY_BINARY_SET.iterator().next(), loadedSet.iterator().next()); + + verify(mockDynamo).getItem(getItemRequestCaptor.capture()); + GetItemRequest getItemRequest = getItemRequestCaptor.getValue(); + assertEquals(KEY_MAP, getItemRequest.key()); + } + + @Test + public void testSaveUsingPut() { + DynamoDBMapper dynamoDBMapper = new DynamoDBMapper(mockDynamo); + + dynamoDBMapper.save(TEST_OBJECT, DynamoDBMapperConfig.builder() + .withSaveBehavior(DynamoDBMapperConfig.SaveBehavior.PUT) + .build()); + + verify(mockDynamo).putItem(putItemRequestCaptor.capture()); + PutItemRequest putItemRequest = putItemRequestCaptor.getValue(); + assertEquals(ITEM_MAP, putItemRequest.item()); + } + + @Test + public void testSaveUsingUpdate() { + when(mockDynamo.updateItem(any(UpdateItemRequest.class))) + .thenReturn(UpdateItemResponse.builder().attributes(ITEM_MAP).build()); + DynamoDBMapper dynamoDBMapper = new DynamoDBMapper(mockDynamo); + + dynamoDBMapper.save(TEST_OBJECT, DynamoDBMapperConfig.builder() + .withSaveBehavior(DynamoDBMapperConfig.SaveBehavior.UPDATE) + .build()); + + verify(mockDynamo).updateItem(updateItemRequestArgumentCaptor.capture()); + UpdateItemRequest updateItemRequest = updateItemRequestArgumentCaptor.getValue(); + assertEquals(KEY_MAP, updateItemRequest.key()); + Map updates = updateItemRequest.attributeUpdates(); + AttributeValueUpdate attributeValueUpdate = updates.get(BINARY_SET_ATTRIBUTE); + assertEquals(EMPTY_BINARY_SET_AV, attributeValueUpdate.value()); + assertEquals("PUT", attributeValueUpdate.actionAsString()); + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/EmptyBinarySetByteBufferAttributesTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/EmptyBinarySetByteBufferAttributesTest.java new file mode 100644 index 000000000000..2179e9e0b74f --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/EmptyBinarySetByteBufferAttributesTest.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.mapper.dynamodb.mapper; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.AttributeValueUpdate; +import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; +import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; +import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; +import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest; +import software.amazon.awssdk.services.dynamodb.model.UpdateItemResponse; +import software.amazon.awssdk.mapper.dynamodb.pojos.BinaryAttributeByteBufferClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +/** + * Tests empty binary set attributes represented as ByteBuffer + */ +@RunWith(MockitoJUnitRunner.class) +public class EmptyBinarySetByteBufferAttributesTest { + + private static final String BINARY_SET_ATTRIBUTE = "binarySetAttribute"; + private static final String KEY_NAME = "key"; + private static final String KEY_VALUE = "test-id"; + private static final ByteBuffer EMPTY_BINARY = ByteBuffer.wrap(new byte[]{}); + private static final Set EMPTY_BINARY_SET; + private static final AttributeValue EMPTY_BINARY_SET_AV = AttributeValue.builder().bs(SdkBytes.fromByteBuffer(EMPTY_BINARY)).build(); + + private static final Map ITEM_MAP; + private static final Map KEY_MAP; + private static final BinaryAttributeByteBufferClass TEST_OBJECT; + + static { + EMPTY_BINARY_SET = new HashSet<>(); + EMPTY_BINARY_SET.add(EMPTY_BINARY); + + KEY_MAP = new HashMap<>(); + KEY_MAP.put(KEY_NAME, AttributeValue.builder().s(KEY_VALUE).build()); + + ITEM_MAP = new HashMap<>(); + ITEM_MAP.put(KEY_NAME, AttributeValue.builder().s(KEY_VALUE).build()); + ITEM_MAP.put(BINARY_SET_ATTRIBUTE, EMPTY_BINARY_SET_AV); + + TEST_OBJECT = new BinaryAttributeByteBufferClass(); + TEST_OBJECT.setKey(KEY_VALUE); + TEST_OBJECT.setBinarySetAttribute(EMPTY_BINARY_SET); + } + + @Mock + private DynamoDbClient mockDynamo; + + @Captor + private ArgumentCaptor getItemRequestCaptor; + + @Captor + private ArgumentCaptor putItemRequestCaptor; + + @Captor + private ArgumentCaptor updateItemRequestArgumentCaptor; + + @Test + public void testLoad() { + when(mockDynamo.getItem(any(GetItemRequest.class))).thenReturn(GetItemResponse.builder().item(ITEM_MAP).build()); + DynamoDBMapper dynamoDBMapper = new DynamoDBMapper(mockDynamo); + + BinaryAttributeByteBufferClass x = dynamoDBMapper.load(BinaryAttributeByteBufferClass.class, + ITEM_MAP.get(KEY_NAME).s()); + assertEquals(ITEM_MAP.get(KEY_NAME).s(), x.getKey()); + assertEquals(EMPTY_BINARY_SET, x.getBinarySetAttribute()); + + verify(mockDynamo).getItem(getItemRequestCaptor.capture()); + GetItemRequest getItemRequest = getItemRequestCaptor.getValue(); + assertEquals(KEY_MAP, getItemRequest.key()); + } + + @Test + public void testSaveUsingPut() { + DynamoDBMapper dynamoDBMapper = new DynamoDBMapper(mockDynamo); + + dynamoDBMapper.save(TEST_OBJECT, DynamoDBMapperConfig.builder() + .withSaveBehavior(DynamoDBMapperConfig.SaveBehavior.PUT) + .build()); + + verify(mockDynamo).putItem(putItemRequestCaptor.capture()); + PutItemRequest putItemRequest = putItemRequestCaptor.getValue(); + assertEquals(ITEM_MAP, putItemRequest.item()); + } + + @Test + public void testSaveUsingUpdate() { + when(mockDynamo.updateItem(any(UpdateItemRequest.class))) + .thenReturn(UpdateItemResponse.builder().attributes(ITEM_MAP).build()); + DynamoDBMapper dynamoDBMapper = new DynamoDBMapper(mockDynamo); + + dynamoDBMapper.save(TEST_OBJECT, DynamoDBMapperConfig.builder() + .withSaveBehavior(DynamoDBMapperConfig.SaveBehavior.UPDATE) + .build()); + + verify(mockDynamo).updateItem(updateItemRequestArgumentCaptor.capture()); + UpdateItemRequest updateItemRequest = updateItemRequestArgumentCaptor.getValue(); + assertEquals(KEY_MAP, updateItemRequest.key()); + Map updates = updateItemRequest.attributeUpdates(); + AttributeValueUpdate attributeValueUpdate = updates.get(BINARY_SET_ATTRIBUTE); + assertEquals(EMPTY_BINARY_SET_AV, attributeValueUpdate.value()); + assertEquals("PUT", attributeValueUpdate.actionAsString()); + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/ExceptionHandlingIntegrationTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/ExceptionHandlingIntegrationTest.java new file mode 100644 index 000000000000..2f6aae85bce4 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/ExceptionHandlingIntegrationTest.java @@ -0,0 +1,566 @@ +/* + * 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.mapper.dynamodb.mapper; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Test; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperIntegrationTestBase; +import software.amazon.awssdk.mapper.dynamodb.ConversionSchemas; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAutoGeneratedKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMappingException; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBRangeKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBVersionAttribute; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; + +/** + * Tests of exception handling + */ +public class ExceptionHandlingIntegrationTest extends DynamoDBMapperIntegrationTestBase { + + public static class NoTableAnnotation { + + private String key; + + @DynamoDBHashKey + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + } + + @Test(expected = DynamoDBMappingException.class) + public void testNoTableAnnotation() throws Exception { + DynamoDBMapper util = new DynamoDBMapper(dynamo); + util.save(new NoTableAnnotation()); + } + + @Test(expected = DynamoDBMappingException.class) + public void testNoTableAnnotationLoad() throws Exception { + DynamoDBMapper util = new DynamoDBMapper(dynamo); + util.load(NoTableAnnotation.class, "abc"); + } + + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class NoDefaultConstructor { + + private String key; + private String attribute; + + @DynamoDBHashKey + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getAttribute() { + return attribute; + } + + public void setAttribute(String attribute) { + this.attribute = attribute; + } + + public NoDefaultConstructor(String key, String attribute) { + super(); + this.key = key; + this.attribute = attribute; + } + } + + @Test(expected = DynamoDBMappingException.class) + public void testNoDefaultConstructor() { + DynamoDBMapper util = new DynamoDBMapper(dynamo); + NoDefaultConstructor obj = new NoDefaultConstructor("" + startKey++, "abc"); + util.save(obj); + util.load(NoDefaultConstructor.class, obj.getKey()); + } + + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class NoKeyGetterDefined { + + @SuppressWarnings("unused") + private String key; + } + + @Test(expected = DynamoDBMappingException.class) + public void testNoHashKeyGetter() throws Exception { + DynamoDBMapper util = new DynamoDBMapper(dynamo); + util.save(new NoKeyGetterDefined()); + } + + @Test(expected = DynamoDBMappingException.class) + public void testNoHashKeyGetterLoad() throws Exception { + DynamoDBMapper util = new DynamoDBMapper(dynamo); + util.load(NoKeyGetterDefined.class, "abc"); + } + + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class PrivateKeyGetter { + + private String key; + + @SuppressWarnings("unused") + @DynamoDBHashKey + private String getKey() { + return key; + } + + @SuppressWarnings("unused") + private void setKey(String key) { + this.key = key; + } + } + + @Test(expected = DynamoDBMappingException.class) + public void testPrivateKeyGetter() throws Exception { + DynamoDBMapper util = new DynamoDBMapper(dynamo); + util.save(new PrivateKeyGetter()); + } + + @Test(expected = DynamoDBMappingException.class) + public void testPrivateKeyGetterLoad() throws Exception { + DynamoDBMapper util = new DynamoDBMapper(dynamo); + util.load(PrivateKeyGetter.class, "abc"); + } + + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class PrivateKeySetter { + + private String key; + + @DynamoDBHashKey + @DynamoDBAutoGeneratedKey + public String getKey() { + return key; + } + + @SuppressWarnings("unused") + private void setKey(String key) { + this.key = key; + } + } + + @Test(expected = DynamoDBMappingException.class) + public void testPrivateKeySetter() throws Exception { + DynamoDBMapper util = new DynamoDBMapper(dynamo); + util.save(new PrivateKeySetter()); + } + + /* + * To trigger this error, we need for a service object to be present, so + * we'll insert one manually. + */ + @Test(expected = DynamoDBMappingException.class) + public void testPrivateKeySetterLoad() throws Exception { + Map attr = new HashMap(); + attr.put(KEY_NAME, AttributeValue.builder().s("abc").build()); + dynamo.putItem(PutItemRequest.builder().tableName("aws-java-sdk-util").item(attr).build()); + DynamoDBMapper util = new DynamoDBMapper(dynamo); + util.load(PrivateKeySetter.class, "abc"); + } + + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class PrivateSetter { + + private String key; + private String StringProperty; + + @DynamoDBHashKey + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getStringProperty() { + return StringProperty; + } + + private void setStringProperty(String stringProperty) { + StringProperty = stringProperty; + } + } + + @Test(expected = DynamoDBMappingException.class) + public void testPrivateSetterLoad() throws Exception { + DynamoDBMapper util = new DynamoDBMapper(dynamo); + PrivateSetter object = new PrivateSetter(); + object.setStringProperty("value"); + util.save(object); + util.load(PrivateSetter.class, object.getKey()); + } + + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class OverloadedSetter { + + private String key; + private String attribute; + + @DynamoDBHashKey + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getAttribute() { + return attribute; + } + + public void setAttribute(String attribute, String unused) { + this.attribute = attribute; + } + } + + @Test(expected = DynamoDBMappingException.class) + public void testOverloadedSetter() { + OverloadedSetter obj = new OverloadedSetter(); + obj.setKey("" + startKey++); + obj.setAttribute("abc", "123"); + DynamoDBMapper mapper = new DynamoDBMapper(dynamo); + mapper.save(obj); + + mapper.load(OverloadedSetter.class, obj.getKey()); + } + + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class WrongTypeForSetter { + + private String key; + private String attribute; + + @DynamoDBHashKey + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getAttribute() { + return attribute; + } + + public void setAttribute(Integer attribute) { + this.attribute = String.valueOf(attribute); + } + + } + + @Test(expected = DynamoDBMappingException.class) + public void testWrongTypeForSetter() { + WrongTypeForSetter obj = new WrongTypeForSetter(); + obj.setKey("" + startKey++); + obj.setAttribute(123); + DynamoDBMapper mapper = new DynamoDBMapper(dynamo); + mapper.save(obj); + + mapper.load(WrongTypeForSetter.class, obj.getKey()); + } + + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class NumericFields { + + private String key; + private Integer integerProperty; + + @DynamoDBHashKey + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public Integer getIntegerProperty() { + return integerProperty; + } + + public void setIntegerProperty(Integer integerProperty) { + this.integerProperty = integerProperty; + } + + } + + @Test(expected = DynamoDBMappingException.class) + public void testWrongDataType() { + Map attr = new HashMap(); + attr.put("integerProperty", AttributeValue.builder().s("abc").build()); + attr.put(KEY_NAME, AttributeValue.builder().s("" + startKey++).build()); + dynamo.putItem(PutItemRequest.builder().tableName("aws-java-sdk-util").item(attr).build()); + DynamoDBMapper util = new DynamoDBMapper(dynamo); + util.load(NumericFields.class, attr.get(KEY_NAME).s()); + } + + @Test(expected = DynamoDBMappingException.class) + public void testWrongDataType2() { + Map attr = new HashMap(); + attr.put("integerProperty", AttributeValue.builder().ns("1", "2", "3").build()); + attr.put(KEY_NAME, AttributeValue.builder().s("" + startKey++).build()); + dynamo.putItem(PutItemRequest.builder().tableName("aws-java-sdk-util").item(attr).build()); + DynamoDBMapper util = new DynamoDBMapper(dynamo); + util.load(NumericFields.class, attr.get(KEY_NAME).s()); + } + + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class ComplexType { + + public String key; + public ComplexType type; + + public ComplexType(String key, ComplexType type) { + super(); + this.key = key; + this.type = type; + } + + @DynamoDBHashKey + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public ComplexType getType() { + return type; + } + + public void setType(ComplexType type) { + this.type = type; + } + } + + // Complex types are not supported by the V1 conversion schema + @Test(expected = DynamoDBMappingException.class) + public void testComplexTypeFailure() { + DynamoDBMapperConfig config = new DynamoDBMapperConfig(ConversionSchemas.V1); + DynamoDBMapper util = new DynamoDBMapper(dynamo, config); + + ComplexType complexType = new ComplexType("" + startKey++, new ComplexType("" + startKey++, null)); + util.save(complexType); + } + + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class ComplexHashKeyType { + + private ComplexType key; + private String attribute; + + @DynamoDBHashKey + public ComplexType getKey() { + return key; + } + + public void setKey(ComplexType key) { + this.key = key; + } + + public String getAttribute() { + return attribute; + } + + public void setAttribute(String attribute) { + this.attribute = attribute; + } + } + + @Test(expected = DynamoDBMappingException.class) + public void testUnsupportedHashKeyType() { + ComplexType complexType = new ComplexType("" + startKey++, new ComplexType("" + startKey++, null)); + ComplexHashKeyType obj = new ComplexHashKeyType(); + obj.setKey(complexType); + obj.setAttribute("abc"); + DynamoDBMapper util = new DynamoDBMapper(dynamo); + util.save(obj); + } + + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class NonSetCollectionType { + + private String key; + private List badlyMapped; + + @DynamoDBHashKey + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public List getBadlyMapped() { + return badlyMapped; + } + + public void setBadlyMapped(List badlyMapped) { + this.badlyMapped = badlyMapped; + } + } + + // Lists are not supported by the V1 conversion schema. + @Test(expected = DynamoDBMappingException.class) + public void testNonSetCollection() { + DynamoDBMapperConfig config = new DynamoDBMapperConfig(ConversionSchemas.V1); + DynamoDBMapper mapper = new DynamoDBMapper(dynamo, config); + + NonSetCollectionType obj = new NonSetCollectionType(); + obj.setKey("" + startKey++); + obj.setBadlyMapped(new ArrayList()); + obj.getBadlyMapped().add("abc"); + mapper.save(obj); + } + + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class FractionalVersionAttribute { + + private String key; + private Double version; + + @DynamoDBHashKey + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + @DynamoDBVersionAttribute + public Double getVersion() { + return version; + } + + public void setVersion(Double version) { + this.version = version; + } + + } + + @Test(expected = DynamoDBMappingException.class) + public void testFractionalVersionAttribute() { + FractionalVersionAttribute obj = new FractionalVersionAttribute(); + obj.setKey("" + startKey++); + obj.setVersion(0d); + DynamoDBMapper mapper = new DynamoDBMapper(dynamo); + mapper.save(obj); + } + + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class AutoGeneratedIntegerKey { + + private Integer key; + private String value; + + @DynamoDBHashKey + @DynamoDBAutoGeneratedKey + public Integer getKey() { + return key; + } + + public void setKey(Integer key) { + this.key = key; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + } + + @Test(expected = DynamoDBMappingException.class) + public void testAutoGeneratedIntegerHashKey() { + AutoGeneratedIntegerKey obj = new AutoGeneratedIntegerKey(); + obj.setValue("fdgfdsgf"); + DynamoDBMapper mapper = new DynamoDBMapper(dynamo); + mapper.save(obj); + } + + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class AutoGeneratedIntegerRangeKey { + + private String key; + private Integer rangekey; + private String value; + + @DynamoDBHashKey + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + @DynamoDBAutoGeneratedKey + @DynamoDBRangeKey + public Integer getRangekey() { + return rangekey; + } + + public void setRangekey(Integer rangekey) { + this.rangekey = rangekey; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + } + + @Test(expected = DynamoDBMappingException.class) + public void testAutoGeneratedIntegerRangeKey() { + AutoGeneratedIntegerRangeKey obj = new AutoGeneratedIntegerRangeKey(); + obj.setKey("Bldadsfa"); + obj.setValue("fdgfdsgf"); + DynamoDBMapper mapper = new DynamoDBMapper(dynamo); + mapper.save(obj); + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/FlattenedIntegrationTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/FlattenedIntegrationTest.java new file mode 100644 index 000000000000..499388d59719 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/FlattenedIntegrationTest.java @@ -0,0 +1,308 @@ +/* + * 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.mapper.dynamodb.mapper; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import software.amazon.awssdk.mapper.dynamodb.pojos.AutoKeyAndVal; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAttribute; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAutoGenerateStrategy; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAutoGeneratedTimestamp; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBFlattened; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMappingException; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; +import software.amazon.awssdk.mapper.dynamodb.pojos.AuditRecord; +import software.amazon.awssdk.mapper.dynamodb.pojos.Currency; +import software.amazon.awssdk.mapper.dynamodb.pojos.DateRange; + +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Set; + +import org.junit.Test; + +/** + * Tests updating component attribute fields correctly. + */ +public class FlattenedIntegrationTest extends AbstractKeyAndValIntegrationTestCase { + + /** + * test object. + */ + @DynamoDBTable(tableName="aws-java-sdk-util") + public static class KeyAndAuditRecord extends AutoKeyAndVal { + public AuditRecord getVal() { + return super.getVal(); + } + @Override + public void setVal(final AuditRecord val) { + super.setVal(val); + } + } + + /** + * Test the mappings. + */ + @Test + public void testAuditRecord() { + final KeyAndAuditRecord object = new KeyAndAuditRecord(); + final AuditRecord auditRecord = assertBeforeAndAfterChange(true, object); + assertNotNull(auditRecord.getCreatedDate()); + assertNotNull(auditRecord.getLastModifiedDate()); + assertEquals(Long.valueOf(1L), auditRecord.getVersionNumber()); + } + + /** + * Test using {@code DateRange}. + */ + @Test + public void testDateRange() throws Exception { + final KeyAndDateRange object = new KeyAndDateRange(); + object.setVal(new DateRange(new Date(), -60000L, 60000L)); + + assertBeforeAndAfterChange(false, object); + } + + /** + * Test using {@code DateRange}. + */ + @Test + public void testDateRangeIsNull() { + final KeyAndDateRange object = new KeyAndDateRange(); + + assertBeforeAndAfterChange(false, object); + } + + /** + * Test using {@code DateRange}. + */ + @Test + public void testDateRangeStartAndEndIsNull() { + final KeyAndDateRange object = new KeyAndDateRange(); + object.setVal(new DateRange()); + + final DateRange after = assertBeforeAndAfterChange(null, object); + assertNull(after); //<- an empty date range produces null object + } + + /** + * Test using {@code DateRange}. + */ + @Test + public void testDateRangeStartIsNull() throws Exception { + final KeyAndDateRange object = new KeyAndDateRange(); + object.setVal(new DateRange(null, new Date())); + + assertBeforeAndAfterChange(false, object); + } + + /** + * Test using {@code DateRange}. + */ + @Test + public void testDateRangeEndIsNull() throws Exception { + final KeyAndDateRange object = new KeyAndDateRange(); + object.setVal(new DateRange(new Date(), null)); + + assertBeforeAndAfterChange(false, object); + } + + /** + * Test using {@code DateRange}. + */ + @Test + public void testAutoDateRange() throws Exception { + final KeyAndAutoDateRange object = new KeyAndAutoDateRange(); + + assertBeforeAndAfterChange(true, object); + } + + /** + * Test using {@code DateRange}. + */ + @Test(expected=DynamoDBMappingException.class) + public void testKeyAndUnknownAttribute() { + final KeyAndUnknownAttribute object = new KeyAndUnknownAttribute(); + object.setVal(new DateRange(new Date(), -60000L, 60000L)); + + assertBeforeAndAfterChange(false, object); + } + + /** + * Test using {@code DateRange}. + */ + @Test(expected=DynamoDBMappingException.class) + public void testDateRangeSet() { + final KeyAndDateRangeSet object = new KeyAndDateRangeSet(); + object.setVal(Collections.singleton(new DateRange(new Date(), -60000L, 60000L))); + + assertBeforeAndAfterChange(false, object); + } + + /** + * Test using {@code DateRange}. + */ + @Test(expected=DynamoDBMappingException.class) + public void testDateRangeList() { + final KeyAndDateRangeList object = new KeyAndDateRangeList(); + object.setVal(Collections.singletonList(new DateRange(new Date(), -60000L, 60000L))); + + assertBeforeAndAfterChange(false, object); + } + + /** + * Test using {@code Currency}. + */ + @Test + public void testKeyAndCurrencies() { + final KeyAndCurrencies object = new KeyAndCurrencies(); + object.setVal(new Currency(1000000D, "CAD")); + object.setOther(new Currency(99.99D, "USD")); + + assertBeforeAndAfterChange(false, object); + + final KeyAndCurrencies reload = util.load(object.getClass(), object.getKey()); + assertEquals(object.getVal(), reload.getVal()); + assertEquals(object.getOther(), reload.getOther()); + } + + /** + * An object with {@code DateRange}. + */ + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class KeyAndDateRange extends AutoKeyAndVal { + @DynamoDBFlattened(attributes={ + @DynamoDBAttribute(mappedBy="start", attributeName="DateRangeStart"), + @DynamoDBAttribute(mappedBy="end", attributeName="DateRangeEnd")}) + public DateRange getVal() { + return super.getVal(); + } + @Override + public void setVal(final DateRange val) { + super.setVal(val); + } + } + + /** + * An object with {@code DateRange}. + */ + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class KeyAndAutoDateRange extends AutoKeyAndVal { + @DynamoDBFlattened(attributes={ + @DynamoDBAttribute(mappedBy="start", attributeName="CreatedDate"), + @DynamoDBAttribute(mappedBy="end", attributeName="LastModifiedDate")}) + public AutoDateRange getVal() { + return super.getVal(); + } + @Override + public void setVal(final AutoDateRange val) { + super.setVal(val); + } + public static class AutoDateRange extends DateRange { + @DynamoDBAutoGeneratedTimestamp(strategy=DynamoDBAutoGenerateStrategy.CREATE) + public Date getStart() { + return super.getStart(); + } + + @DynamoDBAutoGeneratedTimestamp(strategy=DynamoDBAutoGenerateStrategy.ALWAYS) + public Date getEnd() { + return super.getEnd(); + } + } + } + + /** + * An object with {@code DateRange}. + */ + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class KeyAndUnknownAttribute extends AutoKeyAndVal { + @DynamoDBFlattened(attributes={ + @DynamoDBAttribute(mappedBy="xstart", attributeName="DateRangeStart"), + @DynamoDBAttribute(mappedBy="end", attributeName="DateRangeEnd")}) + public DateRange getVal() { + return super.getVal(); + } + @Override + public void setVal(final DateRange val) { + super.setVal(val); + } + } + + /** + * An object with {@code DateRange}. + */ + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class KeyAndDateRangeSet extends AutoKeyAndVal> { + @DynamoDBFlattened(attributes={ + @DynamoDBAttribute(mappedBy="start", attributeName="starts"), + @DynamoDBAttribute(mappedBy="end", attributeName="ends")}) + public Set getVal() { + return super.getVal(); + } + @Override + public void setVal(final Set val) { + super.setVal(val); + } + } + + /** + * An object with {@code DateRange}. + */ + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class KeyAndDateRangeList extends AutoKeyAndVal> { + @DynamoDBFlattened(attributes={ + @DynamoDBAttribute(mappedBy="start", attributeName="starts"), + @DynamoDBAttribute(mappedBy="end", attributeName="ends")}) + public List getVal() { + return super.getVal(); + } + @Override + public void setVal(final List val) { + super.setVal(val); + } + } + + /** + * An object with {@code Currency}. + */ + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class KeyAndCurrencies extends AutoKeyAndVal { + private Currency other; + @DynamoDBFlattened(attributes={ + @DynamoDBAttribute(mappedBy="amount", attributeName="firstAmount"), + @DynamoDBAttribute(mappedBy="unit", attributeName="firstUnit")}) + public Currency getVal() { + return super.getVal(); + } + @Override + public void setVal(final Currency val) { + super.setVal(val); + } + @DynamoDBFlattened(attributes={ + @DynamoDBAttribute(mappedBy="amount", attributeName="secondAmount"), + @DynamoDBAttribute(mappedBy="unit", attributeName="secondUnit")}) + public Currency getOther() { + return this.other; + } + public void setOther(final Currency other) { + this.other = other; + } + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/GenerateCreateTableRequest2Test.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/GenerateCreateTableRequest2Test.java new file mode 100644 index 000000000000..9022969d2eb0 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/GenerateCreateTableRequest2Test.java @@ -0,0 +1,257 @@ +/* + * 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.mapper.dynamodb.mapper; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; +import software.amazon.awssdk.mapper.dynamodb.LocalDynamoDBTestBase; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper; +import com.amazonaws.services.dynamodbv2.model.AttributeDefinition; +import com.amazonaws.services.dynamodbv2.model.CreateTableRequest; +import com.amazonaws.services.dynamodbv2.model.GlobalSecondaryIndex; +import com.amazonaws.services.dynamodbv2.model.GlobalSecondaryIndexDescription; +import com.amazonaws.services.dynamodbv2.model.KeySchemaElement; +import com.amazonaws.services.dynamodbv2.model.KeyType; +import com.amazonaws.services.dynamodbv2.model.LocalSecondaryIndex; +import com.amazonaws.services.dynamodbv2.model.LocalSecondaryIndexDescription; +import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput; +import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType; +import com.amazonaws.services.dynamodbv2.model.TableDescription; +import com.amazonaws.services.dynamodbv2.util.TableUtils; +import software.amazon.awssdk.mapper.dynamodb.test.util.UnorderedCollectionComparator; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * Tests that the CreateTableRequest generated by DynamoDBMapper.generateCreateTableRequest + * correctly creates the expected table. + */ +public class GenerateCreateTableRequest2Test extends LocalDynamoDBTestBase { + + private static final Set testedTableName = new HashSet(); + private static final ProvisionedThroughput DEFAULT_CAPACITY = new ProvisionedThroughput(5L, 5L); + private static DynamoDBMapper mapper; + private static AmazonDynamoDB dynamo; + + @BeforeClass + public static void setUp() throws Exception { + dynamo = client(); + mapper = new DynamoDBMapper(dynamo); + } + + @AfterClass + public static void tearDown() { + for (String tableName : testedTableName) { + dynamo.deleteTable(tableName); + } + } + + @Test + public void testParseIndexRangeKeyClass() throws Exception { + CreateTableRequest request = mapper.generateCreateTableRequest(IndexRangeKeyClass.class); + String createdTableName = appendCurrentTimeToTableName(request); + testedTableName.add(createdTableName); + setProvisionedThroughput(request, DEFAULT_CAPACITY); + + TableDescription createdTableDescription = dynamo.createTable(request).getTableDescription(); + + assertEquals(createdTableName, createdTableDescription.getTableName()); + List expectedKeyElements = Arrays.asList( + new KeySchemaElement("key", KeyType.HASH), + new KeySchemaElement("rangeKey", KeyType.RANGE) + ); + assertEquals(expectedKeyElements, createdTableDescription.getKeySchema()); + + List expectedAttrDefinitions = Arrays.asList( + new AttributeDefinition("key", ScalarAttributeType.N), + new AttributeDefinition("rangeKey", ScalarAttributeType.N), + new AttributeDefinition("indexFooRangeKey", ScalarAttributeType.N), + new AttributeDefinition("indexBarRangeKey", ScalarAttributeType.N), + new AttributeDefinition("multipleIndexRangeKey", ScalarAttributeType.N) + ); + assertTrue(UnorderedCollectionComparator.equalUnorderedCollections( + expectedAttrDefinitions, + createdTableDescription.getAttributeDefinitions())); + + List expectedLsi = Arrays.asList( + new LocalSecondaryIndex() + .withIndexName("index_foo") + .withKeySchema( + new KeySchemaElement("key", KeyType.HASH), + new KeySchemaElement("indexFooRangeKey", KeyType.RANGE)), + new LocalSecondaryIndex() + .withIndexName("index_bar") + .withKeySchema( + new KeySchemaElement("key", KeyType.HASH), + new KeySchemaElement("indexBarRangeKey", KeyType.RANGE)), + new LocalSecondaryIndex() + .withIndexName("index_foo_copy") + .withKeySchema( + new KeySchemaElement("key", KeyType.HASH), + new KeySchemaElement("multipleIndexRangeKey", KeyType.RANGE)), + new LocalSecondaryIndex() + .withIndexName("index_bar_copy") + .withKeySchema( + new KeySchemaElement("key", KeyType.HASH), + new KeySchemaElement("multipleIndexRangeKey", KeyType.RANGE))); + assertTrue(equalLsi(expectedLsi, createdTableDescription.getLocalSecondaryIndexes())); + + assertNull(request.getGlobalSecondaryIndexes()); + assertEquals(DEFAULT_CAPACITY, request.getProvisionedThroughput()); + + // Only one table with indexes can be created simultaneously + TableUtils.waitUntilActive(dynamo, createdTableName); + } + + @Test + public void testComplexIndexedHashRangeClass() throws Exception { + CreateTableRequest request = mapper.generateCreateTableRequest(MapperQueryExpressionTest.HashRangeClass.class); + String createdTableName = appendCurrentTimeToTableName(request); + testedTableName.add(createdTableName); + setProvisionedThroughput(request, DEFAULT_CAPACITY); + + TableDescription createdTableDescription = dynamo.createTable(request).getTableDescription(); + + assertEquals(createdTableName, createdTableDescription.getTableName()); + List expectedKeyElements = Arrays.asList( + new KeySchemaElement("primaryHashKey", KeyType.HASH), + new KeySchemaElement("primaryRangeKey", KeyType.RANGE) + ); + assertEquals(expectedKeyElements, createdTableDescription.getKeySchema()); + + List expectedAttrDefinitions = Arrays.asList( + new AttributeDefinition("primaryHashKey", ScalarAttributeType.S), + new AttributeDefinition("indexHashKey", ScalarAttributeType.S), + new AttributeDefinition("primaryRangeKey", ScalarAttributeType.S), + new AttributeDefinition("indexRangeKey", ScalarAttributeType.S), + new AttributeDefinition("anotherIndexRangeKey", ScalarAttributeType.S) + ); + assertTrue(UnorderedCollectionComparator.equalUnorderedCollections( + expectedAttrDefinitions, + createdTableDescription.getAttributeDefinitions())); + + List expectedLsi = Arrays.asList( + new LocalSecondaryIndex() + .withIndexName("LSI-primary-range") + .withKeySchema( + new KeySchemaElement("primaryHashKey", KeyType.HASH), + new KeySchemaElement("primaryRangeKey", KeyType.RANGE)), + new LocalSecondaryIndex() + .withIndexName("LSI-index-range-1") + .withKeySchema( + new KeySchemaElement("primaryHashKey", KeyType.HASH), + new KeySchemaElement("indexRangeKey", KeyType.RANGE)), + new LocalSecondaryIndex() + .withIndexName("LSI-index-range-2") + .withKeySchema( + new KeySchemaElement("primaryHashKey", KeyType.HASH), + new KeySchemaElement("indexRangeKey", KeyType.RANGE)), + new LocalSecondaryIndex() + .withIndexName("LSI-index-range-3") + .withKeySchema( + new KeySchemaElement("primaryHashKey", KeyType.HASH), + new KeySchemaElement("anotherIndexRangeKey", KeyType.RANGE))); + assertTrue(equalLsi(expectedLsi, createdTableDescription.getLocalSecondaryIndexes())); + + List expectedGsi = Arrays.asList( + new GlobalSecondaryIndex() + .withIndexName("GSI-primary-hash-index-range-1") + .withKeySchema( + new KeySchemaElement("primaryHashKey", KeyType.HASH), + new KeySchemaElement("indexRangeKey", KeyType.RANGE)), + new GlobalSecondaryIndex() + .withIndexName("GSI-primary-hash-index-range-2") + .withKeySchema( + new KeySchemaElement("primaryHashKey", KeyType.HASH), + new KeySchemaElement("anotherIndexRangeKey", KeyType.RANGE)), + new GlobalSecondaryIndex() + .withIndexName("GSI-index-hash-primary-range") + .withKeySchema( + new KeySchemaElement("indexHashKey", KeyType.HASH), + new KeySchemaElement("primaryRangeKey", KeyType.RANGE)), + new GlobalSecondaryIndex() + .withIndexName("GSI-index-hash-index-range-1") + .withKeySchema( + new KeySchemaElement("indexHashKey", KeyType.HASH), + new KeySchemaElement("indexRangeKey", KeyType.RANGE)), + new GlobalSecondaryIndex() + .withIndexName("GSI-index-hash-index-range-2") + .withKeySchema( + new KeySchemaElement("indexHashKey", KeyType.HASH), + new KeySchemaElement("indexRangeKey", KeyType.RANGE))); + assertTrue(equalGsi(expectedGsi, createdTableDescription.getGlobalSecondaryIndexes())); + + assertEquals(DEFAULT_CAPACITY, request.getProvisionedThroughput()); + + // Only one table with indexes can be created simultaneously + TableUtils.waitUntilActive(dynamo, createdTableName); + } + + private static void setProvisionedThroughput(CreateTableRequest request, ProvisionedThroughput throughput) { + request.setProvisionedThroughput(throughput); + if (request.getGlobalSecondaryIndexes() != null) { + for (GlobalSecondaryIndex gsi : request.getGlobalSecondaryIndexes()) { + gsi.setProvisionedThroughput(throughput); + } + } + } + + private static boolean equalLsi(Collection a, Collection b) { + return UnorderedCollectionComparator.equalUnorderedCollections(a, b, new LocalSecondaryIndexDefinitionComparator()); + } + + private static boolean equalGsi(Collection a, Collection b) { + return UnorderedCollectionComparator.equalUnorderedCollections(a, b, new GlobalSecondaryIndexDefinitionComparator()); + } + + private static class LocalSecondaryIndexDefinitionComparator + implements + UnorderedCollectionComparator.CrossTypeComparator { + + @Override + public boolean equals(LocalSecondaryIndex a, LocalSecondaryIndexDescription b) { + return a.getIndexName().equals(b.getIndexName()) + && a.getKeySchema().equals(b.getKeySchema()); + } + + } + + private static class GlobalSecondaryIndexDefinitionComparator + implements + UnorderedCollectionComparator.CrossTypeComparator { + + @Override + public boolean equals(GlobalSecondaryIndex a, GlobalSecondaryIndexDescription b) { + return a.getIndexName().equals(b.getIndexName()) + && a.getKeySchema().equals(b.getKeySchema()); + } + } + + private static String appendCurrentTimeToTableName(CreateTableRequest request) { + String appendedName = String.format("%s-%d", request.getTableName(), System.currentTimeMillis()); + request.setTableName(appendedName); + return appendedName; + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/GenerateCreateTableRequestTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/GenerateCreateTableRequestTest.java new file mode 100644 index 000000000000..0ee883081ae5 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/GenerateCreateTableRequestTest.java @@ -0,0 +1,210 @@ +/* + * 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.mapper.dynamodb.mapper; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.junit.BeforeClass; +import org.junit.Test; + +import com.amazonaws.auth.AnonymousAWSCredentials; +import software.amazon.awssdk.mapper.dynamodb.test.util.DynamoDBTestBase; +import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper; +import com.amazonaws.services.dynamodbv2.model.AttributeDefinition; +import com.amazonaws.services.dynamodbv2.model.CreateTableRequest; +import com.amazonaws.services.dynamodbv2.model.GlobalSecondaryIndex; +import com.amazonaws.services.dynamodbv2.model.KeySchemaElement; +import com.amazonaws.services.dynamodbv2.model.KeyType; +import com.amazonaws.services.dynamodbv2.model.LocalSecondaryIndex; +import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType; +import software.amazon.awssdk.mapper.dynamodb.test.util.UnorderedCollectionComparator; + +/** + * Tests on the DynamoDBMapper.generateCreateTableRequest method. + */ +public class GenerateCreateTableRequestTest extends DynamoDBTestBase { + + private static DynamoDBMapper mapper; + + @BeforeClass + public static void setUp() { + dynamo = new AmazonDynamoDBClient(new AnonymousAWSCredentials()); + mapper = new DynamoDBMapper(dynamo); + } + + @Test + public void testParseIndexRangeKeyClass() { + CreateTableRequest request = mapper.generateCreateTableRequest(IndexRangeKeyClass.class); + + assertEquals("aws-java-sdk-index-range-test", request.getTableName()); + List expectedKeyElements = Arrays.asList( + new KeySchemaElement("key", KeyType.HASH), + new KeySchemaElement("rangeKey", KeyType.RANGE) + ); + assertEquals(expectedKeyElements, request.getKeySchema()); + + List expectedAttrDefinitions = Arrays.asList( + new AttributeDefinition("key", ScalarAttributeType.N), + new AttributeDefinition("rangeKey", ScalarAttributeType.N), + new AttributeDefinition("indexFooRangeKey", ScalarAttributeType.N), + new AttributeDefinition("indexBarRangeKey", ScalarAttributeType.N), + new AttributeDefinition("multipleIndexRangeKey", ScalarAttributeType.N) + ); + assertTrue(UnorderedCollectionComparator.equalUnorderedCollections( + expectedAttrDefinitions, + request.getAttributeDefinitions())); + + List expectedLsi = Arrays.asList( + new LocalSecondaryIndex() + .withIndexName("index_foo") + .withKeySchema( + new KeySchemaElement("key", KeyType.HASH), + new KeySchemaElement("indexFooRangeKey", KeyType.RANGE)), + new LocalSecondaryIndex() + .withIndexName("index_bar") + .withKeySchema( + new KeySchemaElement("key", KeyType.HASH), + new KeySchemaElement("indexBarRangeKey", KeyType.RANGE)), + new LocalSecondaryIndex() + .withIndexName("index_foo_copy") + .withKeySchema( + new KeySchemaElement("key", KeyType.HASH), + new KeySchemaElement("multipleIndexRangeKey", KeyType.RANGE)), + new LocalSecondaryIndex() + .withIndexName("index_bar_copy") + .withKeySchema( + new KeySchemaElement("key", KeyType.HASH), + new KeySchemaElement("multipleIndexRangeKey", KeyType.RANGE))); + assertTrue(equalLsi(expectedLsi, request.getLocalSecondaryIndexes())); + + assertNull(request.getGlobalSecondaryIndexes()); + assertNull(request.getProvisionedThroughput()); + } + + @Test + public void testComplexIndexedHashRangeClass() { + CreateTableRequest request = mapper.generateCreateTableRequest(MapperQueryExpressionTest.HashRangeClass.class); + + assertEquals("table_name", request.getTableName()); + List expectedKeyElements = Arrays.asList( + new KeySchemaElement("primaryHashKey", KeyType.HASH), + new KeySchemaElement("primaryRangeKey", KeyType.RANGE) + ); + assertEquals(expectedKeyElements, request.getKeySchema()); + + List expectedAttrDefinitions = Arrays.asList( + new AttributeDefinition("primaryHashKey", ScalarAttributeType.S), + new AttributeDefinition("indexHashKey", ScalarAttributeType.S), + new AttributeDefinition("primaryRangeKey", ScalarAttributeType.S), + new AttributeDefinition("indexRangeKey", ScalarAttributeType.S), + new AttributeDefinition("anotherIndexRangeKey", ScalarAttributeType.S) + ); + assertTrue(UnorderedCollectionComparator.equalUnorderedCollections( + expectedAttrDefinitions, + request.getAttributeDefinitions())); + + List expectedLsi = Arrays.asList( + new LocalSecondaryIndex() + .withIndexName("LSI-primary-range") + .withKeySchema( + new KeySchemaElement("primaryHashKey", KeyType.HASH), + new KeySchemaElement("primaryRangeKey", KeyType.RANGE)), + new LocalSecondaryIndex() + .withIndexName("LSI-index-range-1") + .withKeySchema( + new KeySchemaElement("primaryHashKey", KeyType.HASH), + new KeySchemaElement("indexRangeKey", KeyType.RANGE)), + new LocalSecondaryIndex() + .withIndexName("LSI-index-range-2") + .withKeySchema( + new KeySchemaElement("primaryHashKey", KeyType.HASH), + new KeySchemaElement("indexRangeKey", KeyType.RANGE)), + new LocalSecondaryIndex() + .withIndexName("LSI-index-range-3") + .withKeySchema( + new KeySchemaElement("primaryHashKey", KeyType.HASH), + new KeySchemaElement("anotherIndexRangeKey", KeyType.RANGE))); + assertTrue(equalLsi(expectedLsi, request.getLocalSecondaryIndexes())); + + List expectedGsi = Arrays.asList( + new GlobalSecondaryIndex() + .withIndexName("GSI-primary-hash-index-range-1") + .withKeySchema( + new KeySchemaElement("primaryHashKey", KeyType.HASH), + new KeySchemaElement("indexRangeKey", KeyType.RANGE)), + new GlobalSecondaryIndex() + .withIndexName("GSI-primary-hash-index-range-2") + .withKeySchema( + new KeySchemaElement("primaryHashKey", KeyType.HASH), + new KeySchemaElement("anotherIndexRangeKey", KeyType.RANGE)), + new GlobalSecondaryIndex() + .withIndexName("GSI-index-hash-primary-range") + .withKeySchema( + new KeySchemaElement("indexHashKey", KeyType.HASH), + new KeySchemaElement("primaryRangeKey", KeyType.RANGE)), + new GlobalSecondaryIndex() + .withIndexName("GSI-index-hash-index-range-1") + .withKeySchema( + new KeySchemaElement("indexHashKey", KeyType.HASH), + new KeySchemaElement("indexRangeKey", KeyType.RANGE)), + new GlobalSecondaryIndex() + .withIndexName("GSI-index-hash-index-range-2") + .withKeySchema( + new KeySchemaElement("indexHashKey", KeyType.HASH), + new KeySchemaElement("indexRangeKey", KeyType.RANGE))); + assertTrue(equalGsi(expectedGsi, request.getGlobalSecondaryIndexes())); + + assertNull(request.getProvisionedThroughput()); + } + + private static boolean equalLsi(Collection a, Collection b) { + return UnorderedCollectionComparator.equalUnorderedCollections(a, b, new LocalSecondaryIndexDefinitionComparator()); + } + + private static boolean equalGsi(Collection a, Collection b) { + return UnorderedCollectionComparator.equalUnorderedCollections(a, b, new GlobalSecondaryIndexDefinitionComparator()); + } + + private static class LocalSecondaryIndexDefinitionComparator + implements + UnorderedCollectionComparator.CrossTypeComparator { + + @Override + public boolean equals(LocalSecondaryIndex a, LocalSecondaryIndex b) { + return a.getIndexName().equals(b.getIndexName()) + && a.getKeySchema().equals(b.getKeySchema()); + } + + } + + private static class GlobalSecondaryIndexDefinitionComparator + implements + UnorderedCollectionComparator.CrossTypeComparator { + + @Override + public boolean equals(GlobalSecondaryIndex a, GlobalSecondaryIndex b) { + return a.getIndexName().equals(b.getIndexName()) + && a.getKeySchema().equals(b.getKeySchema()); + } + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/HashKeyOnlyTableWithGSITest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/HashKeyOnlyTableWithGSITest.java new file mode 100644 index 000000000000..b1ece1da1b70 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/HashKeyOnlyTableWithGSITest.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.mapper.dynamodb.mapper; + +import static org.junit.Assert.assertEquals; + +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.mapper.dynamodb.LocalDynamoDBTestBase; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBIndexHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBIndexRangeKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBQueryExpression; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; +import software.amazon.awssdk.mapper.dynamodb.PaginatedQueryList; +import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.ComparisonOperator; +import software.amazon.awssdk.services.dynamodb.model.Condition; +import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest; +import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; +import software.amazon.awssdk.services.dynamodb.model.GlobalSecondaryIndex; +import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement; +import software.amazon.awssdk.services.dynamodb.model.KeyType; +import software.amazon.awssdk.services.dynamodb.model.Projection; +import software.amazon.awssdk.services.dynamodb.model.ProjectionType; +import software.amazon.awssdk.services.dynamodb.model.ProvisionedThroughput; +import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * Integration test for GSI support with a table that has no primary range key (only a primary hash key). + */ +public class HashKeyOnlyTableWithGSITest extends LocalDynamoDBTestBase { + + public static final String HASH_KEY_ONLY_TABLE_NAME = "no-primary-range-key-gsi-test"; + private static DynamoDbClient dynamo; + + + @BeforeClass + public static void setUp() throws Exception { + dynamo = client(); + CreateTableRequest req = CreateTableRequest.builder() + .tableName(HASH_KEY_ONLY_TABLE_NAME) + .keySchema(KeySchemaElement.builder().attributeName("id").keyType(KeyType.HASH).build()) + .provisionedThroughput(ProvisionedThroughput.builder() + .readCapacityUnits(10L).writeCapacityUnits(10L).build()) + .attributeDefinitions( + AttributeDefinition.builder().attributeName("id").attributeType(ScalarAttributeType.S).build(), + AttributeDefinition.builder().attributeName("status").attributeType(ScalarAttributeType.S).build(), + AttributeDefinition.builder().attributeName("ts").attributeType(ScalarAttributeType.S).build()) + .globalSecondaryIndexes( + GlobalSecondaryIndex.builder() + .provisionedThroughput(ProvisionedThroughput.builder() + .readCapacityUnits(10L).writeCapacityUnits(10L).build()) + .indexName("statusAndCreation") + .keySchema( + KeySchemaElement.builder().attributeName("status").keyType(KeyType.HASH).build(), + KeySchemaElement.builder().attributeName("ts").keyType(KeyType.RANGE).build()) + .projection( + Projection.builder().projectionType(ProjectionType.ALL).build()) + .build()) + .build(); + + dynamo.createTable(req); + } + + @AfterClass + public static void tearDown() throws Exception { + dynamo.deleteTable(DeleteTableRequest.builder().tableName(HASH_KEY_ONLY_TABLE_NAME).build()); + } + + @DynamoDBTable(tableName = HASH_KEY_ONLY_TABLE_NAME) + public static class User { + private String id; + private String status; + private String ts; + + @DynamoDBHashKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @DynamoDBIndexHashKey(globalSecondaryIndexName = "statusAndCreation") + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + @DynamoDBIndexRangeKey(globalSecondaryIndexName = "statusAndCreation") + public String getTs() { + return ts; + } + + public void setTs(String ts) { + this.ts = ts; + } + } + + + /** Tests that we can query using the hash/range GSI on our hash-key only table. */ + @Test + public void testGSIQuery() throws Exception { + DynamoDBMapper mapper = new DynamoDBMapper(dynamo); + String status = "foo-status"; + + User user = new User(); + user.setId("123"); + user.setStatus(status); + user.setTs("321"); + mapper.save(user); + + PaginatedQueryList queryResult; + long endTime = System.currentTimeMillis() + 1000 * 60; + do { + DynamoDBQueryExpression expr = new DynamoDBQueryExpression() + .withIndexName("statusAndCreation") + .withLimit(100) + .withConsistentRead(false) + .withHashKeyValues(user) + .withRangeKeyCondition("ts", + Condition.builder() + .comparisonOperator(ComparisonOperator.GT) + .attributeValueList(AttributeValue.builder().s("100").build()) + .build()); + + queryResult = mapper.query(User.class, expr); + } while (queryResult.size() == 0 && System.currentTimeMillis() < endTime); + + assertEquals(1, queryResult.size()); + assertEquals(status, queryResult.get(0).getStatus()); + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/IndexRangeKeyAttributesIntegrationTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/IndexRangeKeyAttributesIntegrationTest.java new file mode 100644 index 000000000000..9451b1478c52 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/IndexRangeKeyAttributesIntegrationTest.java @@ -0,0 +1,408 @@ +/* + * 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.mapper.dynamodb.mapper; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.junit.BeforeClass; +import org.junit.Test; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperIntegrationTestBase; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.ConsistentReads; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMappingException; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBQueryExpression; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.ComparisonOperator; +import software.amazon.awssdk.services.dynamodb.model.Condition; +import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; + +/** + * Tests that index range keys are properly handled as common attribute + * when items are loaded, saved/updated by using primary key. + * Also tests using index range keys for queries. + */ +public class IndexRangeKeyAttributesIntegrationTest extends DynamoDBMapperIntegrationTestBase { + + private static DynamoDBMapper mapper; + private static final String RANGE_KEY = "rangeKey"; + private static final String INDEX_FOO_RANGE_KEY = "indexFooRangeKey"; + private static final String INDEX_BAR_RANGE_KEY = "indexBarRangeKey"; + private static final String MULTIPLE_INDEX_RANGE_KEY = "multipleIndexRangeKey"; + private static final String FOO_ATTRIBUTE = "fooAttribute"; + private static final String BAR_ATTRIBUTE = "barAttribute"; + private static final String VERSION_ATTRIBUTE = "version"; + + // We don't start with the current system millis like other tests because + // it's out of the range of some data types + private static int start = 1; + + private static final List> attrs = new LinkedList>(); + private static final List hashKeyValues = new LinkedList(); + private static final int totalHash = 5; + private static final int rangePerHash = 64; + private static final int indexFooRangeStep = 2; + private static final int indexBarRangeStep = 4; + private static final int multipleIndexRangeStep = 8; + // Test data + static { + for ( int i = 0; i < totalHash; i++ ) { + long hashKeyValue = startKey++; + hashKeyValues.add(hashKeyValue); + for (int j = 0; j< rangePerHash; j++) { + Map attr = new HashMap(); + attr.put(KEY_NAME, AttributeValue.builder().n("" + hashKeyValue).build()); + attr.put(RANGE_KEY, AttributeValue.builder().n("" + j).build()); + if ( j % indexFooRangeStep == 0) + attr.put(INDEX_FOO_RANGE_KEY, AttributeValue.builder().n("" + j).build()); + if ( j % indexBarRangeStep == 0) + attr.put(INDEX_BAR_RANGE_KEY, AttributeValue.builder().n("" + j).build()); + if ( j % multipleIndexRangeStep == 0) + attr.put(MULTIPLE_INDEX_RANGE_KEY, AttributeValue.builder().n("" + j).build()); + attr.put(FOO_ATTRIBUTE, AttributeValue.builder().s(UUID.randomUUID().toString()).build()); + attr.put(BAR_ATTRIBUTE, AttributeValue.builder().s(UUID.randomUUID().toString()).build()); + attr.put(VERSION_ATTRIBUTE, AttributeValue.builder().n("1").build()); + + attrs.add(attr); + } + } + }; + + @BeforeClass + public static void setUp() throws Exception { + boolean recreateTable = false; + setUpTableWithIndexRangeAttribute(recreateTable); + + // Insert the data + for ( Map attr : attrs ) { + dynamo.putItem(PutItemRequest.builder().tableName(TABLE_WITH_INDEX_RANGE_ATTRIBUTE).item(attr).build()); + } + + mapper = new DynamoDBMapper(dynamo, + new DynamoDBMapperConfig(ConsistentReads.CONSISTENT)); + } + + /** + * Tests that attribute annotated with @DynamoDBIndexRangeKey is properly set in the loaded object. + */ + @Test + public void testLoad() throws Exception { + for ( Map attr : attrs ) { + IndexRangeKeyClass x = mapper.load(newIndexRangeKey(Long.parseLong(attr.get(KEY_NAME).n()), + Double.parseDouble(attr.get(RANGE_KEY).n()))); + + // Convert all numbers to the most inclusive type for easy + // comparison + assertEquals(new BigDecimal(x.getKey()), new BigDecimal(attr.get(KEY_NAME).n())); + assertEquals(new BigDecimal(x.getRangeKey()), new BigDecimal(attr.get(RANGE_KEY).n())); + if (null == attr.get(INDEX_FOO_RANGE_KEY)) + assertNull(x.getIndexFooRangeKeyWithFakeName()); + else + assertEquals(new BigDecimal(x.getIndexFooRangeKeyWithFakeName()), new BigDecimal(attr.get(INDEX_FOO_RANGE_KEY).n())); + if (null == attr.get(INDEX_BAR_RANGE_KEY)) + assertNull(x.getIndexBarRangeKey()); + else + assertEquals(new BigDecimal(x.getIndexBarRangeKey()), new BigDecimal(attr.get(INDEX_BAR_RANGE_KEY).n())); + assertEquals(new BigDecimal(x.getVersion()), new BigDecimal(attr.get(VERSION_ATTRIBUTE).n())); + assertEquals(x.getFooAttribute(), attr.get(FOO_ATTRIBUTE).s()); + assertEquals(x.getBarAttribute(), attr.get(BAR_ATTRIBUTE).s()); + + } + } + + private IndexRangeKeyClass newIndexRangeKey(long hashKey, double rangeKey) { + IndexRangeKeyClass obj = new IndexRangeKeyClass(); + obj.setKey(hashKey); + obj.setRangeKey(rangeKey); + return obj; + } + + /** + * Tests that attribute annotated with @DynamoDBIndexRangeKey is properly saved. + */ + @Test + public void testSave() throws Exception { + List objs = new ArrayList(); + for ( int i = 0; i < 5; i++ ) { + IndexRangeKeyClass obj = getUniqueObject(); + objs.add(obj); + } + + for (IndexRangeKeyClass obj : objs) { + mapper.save(obj); + } + + for (IndexRangeKeyClass obj : objs) { + IndexRangeKeyClass loaded = mapper.load(IndexRangeKeyClass.class, obj.getKey(), obj.getRangeKey()); + assertEquals(obj, loaded); + } + } + + /** + * Tests that version attribute is still working as expected. + */ + @Test + public void testUpdate() throws Exception { + List objs = new ArrayList(); + for ( int i = 0; i < 5; i++ ) { + IndexRangeKeyClass obj = getUniqueObject(); + objs.add(obj); + } + + for (IndexRangeKeyClass obj : objs) { + mapper.save(obj); + } + + for ( IndexRangeKeyClass obj : objs ) { + IndexRangeKeyClass replacement = getUniqueObject(); + replacement.setKey(obj.getKey()); + replacement.setRangeKey(obj.getRangeKey()); + replacement.setVersion(obj.getVersion()); + mapper.save(replacement); + + IndexRangeKeyClass loadedObject = mapper.load(IndexRangeKeyClass.class, obj.getKey(), obj.getRangeKey()); + assertEquals(replacement, loadedObject); + + // If we try to update the old version, we should get an error + replacement.setVersion(replacement.getVersion() - 1); + try { + mapper.save(replacement); + fail("Should have thrown an exception"); + } catch ( Exception expected ) { + } + } + } + + /** + * Tests making queries on local secondary index + */ + @Test + public void testQueryWithIndexRangekey() { + int indexFooRangePerHash = rangePerHash / indexFooRangeStep; + int indexBarRangePerHash = rangePerHash / indexBarRangeStep; + for ( long hashKeyValue : hashKeyValues ) { + IndexRangeKeyClass hashKeyItem = new IndexRangeKeyClass(); + hashKeyItem.setKey(hashKeyValue); + + /** + * Query items by primary range key + */ + List result = mapper.query(IndexRangeKeyClass.class, + new DynamoDBQueryExpression() + .withHashKeyValues(hashKeyItem) + .withRangeKeyCondition(RANGE_KEY, + Condition.builder() + .attributeValueList(AttributeValue.builder().n("0").build()) + .comparisonOperator(ComparisonOperator.GE.toString()).build())); + assertTrue(rangePerHash == result.size()); + // check that all attributes are retrieved + for (IndexRangeKeyClass itemInFooIndex : result) { + assertNotNull(itemInFooIndex.getFooAttribute()); + assertNotNull(itemInFooIndex.getBarAttribute()); + } + + /** + * Query items on index_foo + */ + result = mapper.query(IndexRangeKeyClass.class, + new DynamoDBQueryExpression() + .withHashKeyValues(hashKeyItem) + .withRangeKeyCondition(INDEX_FOO_RANGE_KEY, + Condition.builder() + .attributeValueList(AttributeValue.builder().n("0").build()) + .comparisonOperator(ComparisonOperator.GE.toString()).build())); + assertTrue(indexFooRangePerHash == result.size()); + // check that only the projected attributes are retrieved + for (IndexRangeKeyClass itemInFooIndex : result) { + assertNotNull(itemInFooIndex.getFooAttribute()); + assertNull(itemInFooIndex.getBarAttribute()); + } + + /** + * Query items on index_bar + */ + result = mapper.query(IndexRangeKeyClass.class, + new DynamoDBQueryExpression() + .withHashKeyValues(hashKeyItem) + .withRangeKeyCondition(INDEX_BAR_RANGE_KEY, + Condition.builder() + .attributeValueList(AttributeValue.builder().n("0").build()) + .comparisonOperator(ComparisonOperator.GE.toString()).build())); + assertTrue(indexBarRangePerHash == result.size()); + // check that only the projected attributes are retrieved + for (IndexRangeKeyClass itemInBarIndex : result) { + assertNull(itemInBarIndex.getFooAttribute()); + assertNotNull(itemInBarIndex.getBarAttribute()); + } + } + } + + /** + * Tests the exception when user specifies an invalid range key name in the query. + */ + @Test + public void testInvalidRangeKeyNameException() { + IndexRangeKeyClass hashKeyItem = new IndexRangeKeyClass(); + hashKeyItem.setKey(0); + try { + mapper.query(IndexRangeKeyClass.class, + new DynamoDBQueryExpression() + .withHashKeyValues(hashKeyItem) + .withRangeKeyCondition("some_range_key", + Condition.builder() + .attributeValueList(AttributeValue.builder().n("0").build()) + .comparisonOperator(ComparisonOperator.GE.toString()).build())); + fail("some_range_key is not a valid range key name."); + } catch (DynamoDBMappingException e) { + System.out.println(e.getMessage()); + } catch (Exception e) { + fail("Should trigger an DynamoDBMappingException."); + } + } + + /** + * Tests the exception when user specifies an invalid index name in the query. + */ + @Test + public void testInvalidIndexNameException() { + IndexRangeKeyClass hashKeyItem = new IndexRangeKeyClass(); + hashKeyItem.setKey(0); + try { + mapper.query(IndexRangeKeyClass.class, + new DynamoDBQueryExpression() + .withHashKeyValues(hashKeyItem) + .withRangeKeyCondition(INDEX_BAR_RANGE_KEY, + Condition.builder() + .attributeValueList(AttributeValue.builder().n("0").build()) + .comparisonOperator(ComparisonOperator.GE.toString()).build()) + .withIndexName("some_index")); + fail("some_index is not a valid index name."); + } catch (IllegalArgumentException iae) { + System.out.println(iae.getMessage()); + } catch (Exception e) { + fail("Should trigger an IllegalArgumentException."); + } + } + + /** + * Tests making queries by using range key that is shared by multiple indexes. + */ + @Test + public void testQueryWithRangeKeyForMultipleIndexes() { + int multipleIndexRangePerHash = rangePerHash / multipleIndexRangeStep; + for ( long hashKeyValue : hashKeyValues ) { + IndexRangeKeyClass hashKeyItem = new IndexRangeKeyClass(); + hashKeyItem.setKey(hashKeyValue); + + /** + * Query items by a range key that is shared by multiple indexes + */ + List result = mapper.query(IndexRangeKeyClass.class, + new DynamoDBQueryExpression() + .withHashKeyValues(hashKeyItem) + .withRangeKeyCondition(MULTIPLE_INDEX_RANGE_KEY, + Condition.builder() + .attributeValueList(AttributeValue.builder().n("0").build()) + .comparisonOperator(ComparisonOperator.GE.toString()).build()) + .withIndexName("index_foo_copy")); + assertTrue(multipleIndexRangePerHash == result.size()); + // check that only the projected attributes are retrieved + for (IndexRangeKeyClass itemInFooIndex : result) { + assertNotNull(itemInFooIndex.getFooAttribute()); + assertNull(itemInFooIndex.getBarAttribute()); + } + result = mapper.query(IndexRangeKeyClass.class, + new DynamoDBQueryExpression() + .withHashKeyValues(hashKeyItem) + .withRangeKeyCondition(MULTIPLE_INDEX_RANGE_KEY, + Condition.builder() + .attributeValueList(AttributeValue.builder().n("0").build()) + .comparisonOperator(ComparisonOperator.GE.toString()).build()) + .withIndexName("index_bar_copy")); + assertTrue(multipleIndexRangePerHash == result.size()); + // check that only the projected attributes are retrieved + for (IndexRangeKeyClass itemInFooIndex : result) { + assertNull(itemInFooIndex.getFooAttribute()); + assertNotNull(itemInFooIndex.getBarAttribute()); + } + + /** + * Exception when user doesn't specify which index to use + */ + try { + mapper.query(IndexRangeKeyClass.class, + new DynamoDBQueryExpression() + .withHashKeyValues(hashKeyItem) + .withRangeKeyCondition(MULTIPLE_INDEX_RANGE_KEY, + Condition.builder() + .attributeValueList(AttributeValue.builder().n("0").build()) + .comparisonOperator(ComparisonOperator.GE.toString()).build())); + fail("No index name is specified when query with a range key shared by multiple indexes"); + } catch (IllegalArgumentException iae) { + System.out.println(iae.getMessage()); + } catch (Exception e) { + fail("Should trigger an IllegalArgumentException."); + } + + /** + * Exception when user uses an invalid index name + */ + try { + mapper.query(IndexRangeKeyClass.class, + new DynamoDBQueryExpression() + .withHashKeyValues(hashKeyItem) + .withRangeKeyCondition(MULTIPLE_INDEX_RANGE_KEY, + Condition.builder() + .attributeValueList(AttributeValue.builder().n("0").build()) + .comparisonOperator(ComparisonOperator.GE.toString()).build()) + .withIndexName("index_foo")); + fail("index_foo is not annotated as part of the localSecondaryIndexNames in " + + "the @DynamoDBIndexRangeKey annotation of multipleIndexRangeKey"); + } catch (IllegalArgumentException iae) { + System.out.println(iae.getMessage()); + } catch (Exception e) { + fail("Should trigger an IllegalArgumentException."); + } + } + + } + + + private IndexRangeKeyClass getUniqueObject() { + IndexRangeKeyClass obj = new IndexRangeKeyClass(); + obj.setKey(startKey++); + obj.setRangeKey((double) start++); + obj.setIndexFooRangeKeyWithFakeName((double) start++); + obj.setIndexBarRangeKey((double) start++); + obj.setFooAttribute("" + startKey++); + obj.setBarAttribute("" + startKey++); + return obj; + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/IndexRangeKeyClass.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/IndexRangeKeyClass.java new file mode 100644 index 000000000000..33609707e61c --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/IndexRangeKeyClass.java @@ -0,0 +1,183 @@ +/* + * 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.mapper.dynamodb.mapper; + +import java.math.BigDecimal; +import java.util.Set; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAttribute; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBIndexRangeKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBRangeKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBVersionAttribute; + +/** + * Comprehensive domain class + */ +@DynamoDBTable(tableName = "aws-java-sdk-index-range-test") +public class IndexRangeKeyClass { + + private long key; + private double rangeKey; + private Double indexFooRangeKey; + private Double indexBarRangeKey; + private Double multipleIndexRangeKey; + private Long version; + + private String fooAttribute; + private String barAttribute; + + @DynamoDBHashKey + public long getKey() { + return key; + } + + public void setKey(long key) { + this.key = key; + } + + @DynamoDBRangeKey + public double getRangeKey() { + return rangeKey; + } + + public void setRangeKey(double rangeKey) { + this.rangeKey = rangeKey; + } + + @DynamoDBIndexRangeKey ( + localSecondaryIndexName = "index_foo", + attributeName = "indexFooRangeKey" + ) + public Double getIndexFooRangeKeyWithFakeName() { + return indexFooRangeKey; + } + + public void setIndexFooRangeKeyWithFakeName(Double indexFooRangeKey) { + this.indexFooRangeKey = indexFooRangeKey; + } + + @DynamoDBIndexRangeKey ( + localSecondaryIndexName = "index_bar" + ) + public Double getIndexBarRangeKey() { + return indexBarRangeKey; + } + + public void setIndexBarRangeKey(Double indexBarRangeKey) { + this.indexBarRangeKey = indexBarRangeKey; + } + + @DynamoDBIndexRangeKey ( + localSecondaryIndexNames = {"index_foo_copy", "index_bar_copy"} + ) + public Double getMultipleIndexRangeKey() { + return multipleIndexRangeKey; + } + + public void setMultipleIndexRangeKey(Double multipleIndexRangeKey) { + this.multipleIndexRangeKey = multipleIndexRangeKey; + } + + @DynamoDBAttribute + public String getFooAttribute() { + return fooAttribute; + } + + public void setFooAttribute(String fooAttribute) { + this.fooAttribute = fooAttribute; + } + + @DynamoDBAttribute + public String getBarAttribute() { + return barAttribute; + } + + public void setBarAttribute(String barAttribute) { + this.barAttribute = barAttribute; + } + + @DynamoDBVersionAttribute + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((fooAttribute == null) ? 0 : fooAttribute.hashCode()); + result = prime * result + ((barAttribute == null) ? 0 : barAttribute.hashCode()); + result = prime * result + (int) (key ^ (key >>> 32)); + long temp; + temp = Double.doubleToLongBits(rangeKey); + result = prime * result + (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(indexFooRangeKey); + result = prime * result + (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(indexBarRangeKey); + result = prime * result + (int) (temp ^ (temp >>> 32)); + result = prime * result + ((version == null) ? 0 : version.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) + return true; + if ( obj == null ) + return false; + if ( getClass() != obj.getClass() ) + return false; + IndexRangeKeyClass other = (IndexRangeKeyClass) obj; + if ( fooAttribute == null ) { + if ( other.fooAttribute != null ) + return false; + } else if ( !fooAttribute.equals(other.fooAttribute) ) + return false; + if ( barAttribute == null ) { + if ( other.barAttribute != null ) + return false; + } else if ( !barAttribute.equals(other.barAttribute) ) + return false; + if ( key != other.key ) + return false; + if ( Double.doubleToLongBits(rangeKey) != Double.doubleToLongBits(other.rangeKey) ) + return false; + if ( Double.doubleToLongBits(indexFooRangeKey) != Double.doubleToLongBits(other.indexFooRangeKey) ) + return false; + if ( Double.doubleToLongBits(indexBarRangeKey) != Double.doubleToLongBits(other.indexBarRangeKey) ) + return false; + if ( version == null ) { + if ( other.version != null ) + return false; + } else if ( !version.equals(other.version) ) + return false; + return true; + } + + @Override + public String toString() { + return "IndexRangeKeyClass [key=" + key + ", rangeKey=" + rangeKey + ", version=" + version + + ", indexFooRangeKey=" + indexFooRangeKey + ", indexBarRangeKey=" + indexBarRangeKey + + ", fooAttribute=" + fooAttribute + ", barAttribute=" + barAttribute + "]"; + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/InheritanceIntegrationTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/InheritanceIntegrationTest.java new file mode 100644 index 000000000000..5a9771439f6d --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/InheritanceIntegrationTest.java @@ -0,0 +1,307 @@ +/* + * 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.mapper.dynamodb.mapper; + +import static software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.ConsistentReads.CONSISTENT; +import static org.junit.Assert.assertEquals; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperIntegrationTestBase; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAttribute; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMappingException; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; + +/** + * Tests inheritance behavior in DynamoDB mapper. + */ +public class InheritanceIntegrationTest extends DynamoDBMapperIntegrationTestBase { + + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class BaseClass { + + protected String key; + protected String normalStringAttribute; + + @DynamoDBHashKey + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + @DynamoDBAttribute + public String getNormalStringAttribute() { + return normalStringAttribute; + } + + public void setNormalStringAttribute(String normalStringAttribute) { + this.normalStringAttribute = normalStringAttribute; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((key == null) ? 0 : key.hashCode()); + result = prime * result + ((normalStringAttribute == null) ? 0 : normalStringAttribute.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) + return true; + if ( obj == null ) + return false; + if ( getClass() != obj.getClass() ) + return false; + BaseClass other = (BaseClass) obj; + if ( key == null ) { + if ( other.key != null ) + return false; + } else if ( !key.equals(other.key) ) + return false; + if ( normalStringAttribute == null ) { + if ( other.normalStringAttribute != null ) + return false; + } else if ( !normalStringAttribute.equals(other.normalStringAttribute) ) + return false; + return true; + } + } + + public static class SubClass extends BaseClass { + + private String subField; + + public String getSubField() { + return subField; + } + + public void setSubField(String subField) { + this.subField = subField; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((subField == null) ? 0 : subField.hashCode()); + return result; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + if ( this == obj ) + return true; + if ( !super.equals(obj) ) + return false; + if ( getClass() != obj.getClass() ) + return false; + SubClass other = (SubClass) obj; + if ( subField == null ) { + if ( other.subField != null ) + return false; + } else if ( !subField.equals(other.subField) ) + return false; + return true; + } + + } + + public static class SubSubClass extends SubClass { + + private String subSubField; + + public String getSubSubField() { + return subSubField; + } + + public void setSubSubField(String subSubField) { + this.subSubField = subSubField; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((subSubField == null) ? 0 : subSubField.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) + return true; + if ( !super.equals(obj) ) + return false; + if ( getClass() != obj.getClass() ) + return false; + SubSubClass other = (SubSubClass) obj; + if ( subSubField == null ) { + if ( other.subSubField != null ) + return false; + } else if ( !subSubField.equals(other.subSubField) ) + return false; + return true; + } + } + + @Test + public void testSubClass() throws Exception { + List objs = new ArrayList(); + for ( int i = 0; i < 5; i++ ) { + SubClass obj = getUniqueObject(new SubClass()); + obj.setSubField("" + startKey++); + objs.add(obj); + } + + DynamoDBMapper util = new DynamoDBMapper(dynamo); + for ( Object obj : objs ) { + util.save(obj); + assertEquals(util.load(SubClass.class, ((SubClass)obj).getKey()), obj); + } + } + + @Test + public void testSubSubClass() throws Exception { + List objs = new ArrayList(); + for ( int i = 0; i < 5; i++ ) { + SubSubClass obj = getUniqueObject(new SubSubClass()); + obj.setSubField("" + startKey++); + obj.setSubSubField("" + startKey++); + objs.add(obj); + } + + DynamoDBMapper util = new DynamoDBMapper(dynamo, DynamoDBMapperConfig.builder() + .withConsistentReads(CONSISTENT) + .build()); + for ( SubSubClass obj : objs ) { + util.save(obj); + assertEquals(util.load(SubSubClass.class, obj.getKey()), obj); + } + } + + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static interface Interface { + + @DynamoDBHashKey + public String getKey(); + + public void setKey(String key); + + @DynamoDBAttribute + public String getAttribute(); + + public void setAttribute(String attribute); + } + + public static class Implementation implements Interface { + + private String key; + private String attribute; + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getAttribute() { + return attribute; + } + + public void setAttribute(String attribute) { + this.attribute = attribute; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((attribute == null) ? 0 : attribute.hashCode()); + result = prime * result + ((key == null) ? 0 : key.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) + return true; + if ( obj == null ) + return false; + if ( getClass() != obj.getClass() ) + return false; + Implementation other = (Implementation) obj; + if ( attribute == null ) { + if ( other.attribute != null ) + return false; + } else if ( !attribute.equals(other.attribute) ) + return false; + if ( key == null ) { + if ( other.key != null ) + return false; + } else if ( !key.equals(other.key) ) + return false; + return true; + } + } + + @Test(expected = DynamoDBMappingException.class) + public void testImplementation() throws Exception { + List objs = new ArrayList(); + for ( int i = 0; i < 5; i++ ) { + Implementation obj = new Implementation(); + obj.setKey("" + startKey++); + obj.setAttribute("" + startKey++); + objs.add(obj); + } + + // Saving new objects with a null version field should populate it + DynamoDBMapper util = new DynamoDBMapper(dynamo); + for ( Interface obj : objs ) { + util.save(obj); + assertEquals(util.load(Implementation.class, obj.getKey()), obj); + } + } + + private T getUniqueObject(T obj) { + obj.setKey("" + startKey++); + obj.setNormalStringAttribute("" + startKey++); + return obj; + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/KeyOnlyPutIntegrationTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/KeyOnlyPutIntegrationTest.java new file mode 100644 index 000000000000..9f2ff6bd960c --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/KeyOnlyPutIntegrationTest.java @@ -0,0 +1,154 @@ +/* + * 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.mapper.dynamodb.mapper; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Test; + +import software.amazon.awssdk.mapper.dynamodb.test.util.DynamoDBIntegrationTestBase; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAttribute; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBSaveExpression; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException; +import software.amazon.awssdk.services.dynamodb.model.ExpectedAttributeValue; + +public class KeyOnlyPutIntegrationTest extends DynamoDBIntegrationTestBase { + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class HashAndAttribute { + + protected String key; + protected String normalStringAttribute; + + @DynamoDBHashKey + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + @DynamoDBAttribute + public String getNormalStringAttribute() { + return normalStringAttribute; + } + + public void setNormalStringAttribute(String normalStringAttribute) { + this.normalStringAttribute = normalStringAttribute; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((key == null) ? 0 : key.hashCode()); + result = prime * result + ((normalStringAttribute == null) ? 0 : normalStringAttribute.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) + return true; + if ( obj == null ) + return false; + if ( getClass() != obj.getClass() ) + return false; + HashAndAttribute other = (HashAndAttribute) obj; + if ( key == null ) { + if ( other.key != null ) + return false; + } else if ( !key.equals(other.key) ) + return false; + if ( normalStringAttribute == null ) { + if ( other.normalStringAttribute != null ) + return false; + } else if ( !normalStringAttribute.equals(other.normalStringAttribute) ) + return false; + return true; + } + } + + @Test + public void testKeyOnlyPut() throws Exception { + /* + * Testing this scenario + * (1) An empty table with the schema: + * + * "key" (HASH) + * + * (2) A POJO class: + * "key" (HASH), "attribute" (NON-KEY) + * + * (3) Save operation by some user: + * - item : {"key" : "some value"} + * - user-specified expected values : {"attribute" : {Exist : true}} + * - SaveBehavior : UPDATE (default) + * + * (4) Expected behavior + * ConditionalCheckFailedException, and the table should remain empty. + */ + List objs = new ArrayList(); + for ( int i = 0; i < 5; i++ ) { + HashAndAttribute obj = getUniqueObject(new HashAndAttribute()); + objs.add(obj); + } + + DynamoDBMapper util = new DynamoDBMapper(dynamo); + for ( HashAndAttribute obj : objs ) { + try { + DynamoDBSaveExpression saveExpression = new DynamoDBSaveExpression(); + Map expected = new HashMap(); + ExpectedAttributeValue expectedVersion = ExpectedAttributeValue.builder() + .value(AttributeValue.builder() + .s("SomeNonExistantValue").build()) + .exists(true).build(); + expected.put("normalStringAttribute", expectedVersion); + saveExpression.setExpected(expected); + + util.save(obj,saveExpression); + fail("This should fail, expected clause should block an insert."); + }catch(ConditionalCheckFailedException e){ + + } + assertNull(util.load(HashAndAttribute.class, obj.getKey())); + + //this should succeed without the expected clause + obj.setNormalStringAttribute("to-be-deleted"); + util.save(obj); + obj.setNormalStringAttribute(null); + util.save(obj); + Object loaded = util.load(HashAndAttribute.class, obj.getKey()); + assertEquals("Expected " + obj.toString() + ", but was " + loaded.toString(), obj, loaded); + } + } + + private T getUniqueObject(T obj) { + obj.setKey("" + startKey++); + return obj; + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/MapperLoadingStrategyConfigTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/MapperLoadingStrategyConfigTest.java new file mode 100644 index 000000000000..fb31f9bfa1ef --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/MapperLoadingStrategyConfigTest.java @@ -0,0 +1,340 @@ +/* + * 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.mapper.dynamodb.mapper; + +import static org.junit.Assert.*; + +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.mapper.dynamodb.LocalDynamoDBTestBase; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; + +import org.junit.BeforeClass; +import org.junit.Test; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.ConsistentReads; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.PaginationLoadingStrategy; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBQueryExpression; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBScanExpression; +import software.amazon.awssdk.mapper.dynamodb.PaginatedList; +import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.ComparisonOperator; +import software.amazon.awssdk.services.dynamodb.model.Condition; +import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest; +import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement; +import software.amazon.awssdk.services.dynamodb.model.KeyType; +import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType; +import software.amazon.awssdk.mapper.dynamodb.pojos.RangeKeyClass; + +/** + * Integration tests for PaginationLoadingStrategy configuration + */ +public class MapperLoadingStrategyConfigTest extends LocalDynamoDBTestBase { + + private static long hashKey = System.currentTimeMillis(); + private static int PAGE_SIZE = 5; + private static int PARALLEL_SEGMENT = 3; + private static int OBJECTS_NUM = 50; + private static int RESULTS_NUM = OBJECTS_NUM - 2; // condition: rangeKey > 1.0 + private static DynamoDbClient dynamo; + + @BeforeClass + public static void setUp() throws Exception { + dynamo = client(); + dynamo.createTable(CreateTableRequest.builder() + .tableName("aws-java-sdk-range-test") + .keySchema(KeySchemaElement.builder().attributeName("key").keyType(KeyType.HASH).build(), + KeySchemaElement.builder().attributeName("rangeKey").keyType(KeyType.RANGE).build()) + .attributeDefinitions(AttributeDefinition.builder().attributeName("key") + .attributeType(ScalarAttributeType.N).build(), + AttributeDefinition.builder().attributeName("rangeKey") + .attributeType(ScalarAttributeType.N).build()) + .provisionedThroughput(DEFAULT_PROVISIONED_THROUGHPUT) + .build()); + createTestData(); + } + + @Test + public void testLazyLoading() { + // Get all the paginated lists using the tested loading strategy + PaginatedList queryList = getTestPaginatedQueryList(PaginationLoadingStrategy.LAZY_LOADING); + PaginatedList scanList = getTestPaginatedScanList(PaginationLoadingStrategy.LAZY_LOADING); + PaginatedList parallelScanList = getTestPaginatedParallelScanList(PaginationLoadingStrategy.LAZY_LOADING); + + // check that only at most one page of results are loaded up to this point + assertTrue(getLoadedResultsNumber(queryList) <= PAGE_SIZE); + assertTrue(getLoadedResultsNumber(scanList) <= PAGE_SIZE); + assertTrue(getLoadedResultsNumber(parallelScanList) <= PAGE_SIZE * PARALLEL_SEGMENT); + + testAllPaginatedListOperations(queryList); + testAllPaginatedListOperations(scanList); + testAllPaginatedListOperations(parallelScanList); + + // Re-construct the paginated lists and test the iterator behavior + queryList = getTestPaginatedQueryList(PaginationLoadingStrategy.LAZY_LOADING); + scanList = getTestPaginatedScanList(PaginationLoadingStrategy.LAZY_LOADING); + parallelScanList = getTestPaginatedParallelScanList(PaginationLoadingStrategy.LAZY_LOADING); + + testPaginatedListIterator(queryList); + testPaginatedListIterator(scanList); + testPaginatedListIterator(parallelScanList); + + } + + @Test + public void testEagerLoading() { + // Get all the paginated lists using the tested loading strategy + PaginatedList queryList = getTestPaginatedQueryList(PaginationLoadingStrategy.EAGER_LOADING); + PaginatedList scanList = getTestPaginatedScanList(PaginationLoadingStrategy.EAGER_LOADING); + PaginatedList parallelScanList = getTestPaginatedParallelScanList(PaginationLoadingStrategy.EAGER_LOADING); + + // check that all results have been loaded + assertEquals(RESULTS_NUM, getLoadedResultsNumber(queryList)); + assertEquals(RESULTS_NUM, getLoadedResultsNumber(scanList)); + assertEquals(RESULTS_NUM, getLoadedResultsNumber(parallelScanList)); + + testAllPaginatedListOperations(queryList); + testAllPaginatedListOperations(scanList); + testAllPaginatedListOperations(parallelScanList); + + // Re-construct the paginated lists and test the iterator behavior + queryList = getTestPaginatedQueryList(PaginationLoadingStrategy.LAZY_LOADING); + scanList = getTestPaginatedScanList(PaginationLoadingStrategy.LAZY_LOADING); + parallelScanList = getTestPaginatedParallelScanList(PaginationLoadingStrategy.LAZY_LOADING); + + testPaginatedListIterator(queryList); + testPaginatedListIterator(scanList); + testPaginatedListIterator(parallelScanList); + } + + @Test + public void testIterationOnly() { + // Get all the paginated lists using the tested loading strategy + PaginatedList queryList = getTestPaginatedQueryList(PaginationLoadingStrategy.ITERATION_ONLY); + PaginatedList scanList = getTestPaginatedScanList(PaginationLoadingStrategy.ITERATION_ONLY); + PaginatedList parallelScanList = getTestPaginatedParallelScanList(PaginationLoadingStrategy.ITERATION_ONLY); + + // check that only at most one page of results are loaded up to this point + assertTrue(getLoadedResultsNumber(queryList) <= PAGE_SIZE); + assertTrue(getLoadedResultsNumber(scanList) <= PAGE_SIZE); + assertTrue(getLoadedResultsNumber(parallelScanList) <= PAGE_SIZE * PARALLEL_SEGMENT); + + testIterationOnlyPaginatedListOperations(queryList); + testIterationOnlyPaginatedListOperations(scanList); + testIterationOnlyPaginatedListOperations(parallelScanList); + } + + private static void createTestData() { + DynamoDBMapper mapper = new DynamoDBMapper(dynamo); + + List objs = new ArrayList(); + for ( int i = 0; i < OBJECTS_NUM; i++ ) { + RangeKeyClass obj = new RangeKeyClass(); + obj.setKey(hashKey); + obj.setRangeKey(i); + objs.add(obj); + } + + mapper.batchSave(objs); + } + + private static PaginatedList getTestPaginatedQueryList(PaginationLoadingStrategy paginationLoadingStrategy) { + DynamoDBMapperConfig mapperConfig = new DynamoDBMapperConfig(ConsistentReads.CONSISTENT); + DynamoDBMapper mapper = new DynamoDBMapper(dynamo, mapperConfig); + + // Construct the query expression for the tested hash-key value and any range-key value greater that 1.0 + RangeKeyClass keyObject = new RangeKeyClass(); + keyObject.setKey(hashKey); + DynamoDBQueryExpression queryExpression = new DynamoDBQueryExpression().withHashKeyValues(keyObject); + queryExpression.withRangeKeyCondition("rangeKey", + Condition.builder().comparisonOperator(ComparisonOperator.GT).attributeValueList( + AttributeValue.builder().n("1.0").build()).build()).withLimit(PAGE_SIZE); + + return mapper.query(RangeKeyClass.class, queryExpression, new DynamoDBMapperConfig(paginationLoadingStrategy)); + } + + private static PaginatedList getTestPaginatedScanList(PaginationLoadingStrategy paginationLoadingStrategy) { + DynamoDBMapperConfig mapperConfig = new DynamoDBMapperConfig(ConsistentReads.CONSISTENT); + DynamoDBMapper mapper = new DynamoDBMapper(dynamo, mapperConfig); + + // Construct the scan expression with the exact same conditions + DynamoDBScanExpression scanExpression = new DynamoDBScanExpression(); + scanExpression.addFilterCondition("key", + Condition.builder().comparisonOperator(ComparisonOperator.EQ).attributeValueList( + AttributeValue.builder().n(Long.toString(hashKey)).build()).build()); + scanExpression.addFilterCondition("rangeKey", + Condition.builder().comparisonOperator(ComparisonOperator.GT).attributeValueList( + AttributeValue.builder().n("1.0").build()).build()); + scanExpression.setLimit(PAGE_SIZE); + + return mapper.scan(RangeKeyClass.class, scanExpression, new DynamoDBMapperConfig(paginationLoadingStrategy)); + } + + private static PaginatedList getTestPaginatedParallelScanList(PaginationLoadingStrategy paginationLoadingStrategy) { + DynamoDBMapperConfig mapperConfig = new DynamoDBMapperConfig(ConsistentReads.CONSISTENT); + DynamoDBMapper mapper = new DynamoDBMapper(dynamo, mapperConfig); + + // Construct the scan expression with the exact same conditions + DynamoDBScanExpression scanExpression = new DynamoDBScanExpression(); + scanExpression.addFilterCondition("key", + Condition.builder().comparisonOperator(ComparisonOperator.EQ).attributeValueList( + AttributeValue.builder().n(Long.toString(hashKey)).build()).build()); + scanExpression.addFilterCondition("rangeKey", + Condition.builder().comparisonOperator(ComparisonOperator.GT).attributeValueList( + AttributeValue.builder().n("1.0").build()).build()); + scanExpression.setLimit(PAGE_SIZE); + + return mapper.parallelScan(RangeKeyClass.class, scanExpression, PARALLEL_SEGMENT, new DynamoDBMapperConfig(paginationLoadingStrategy)); + } + + private static void testAllPaginatedListOperations(PaginatedList list) { + + // (1) isEmpty() + assertFalse(list.isEmpty()); + + // (2) get(int n) + assertNotNull(list.get(RESULTS_NUM / 2)); + + // (3) contains(Object org0) + RangeKeyClass obj = new RangeKeyClass(); + obj.setKey(hashKey); + obj.setRangeKey(0); + assertFalse(list.contains(obj)); + obj.setRangeKey(2); + assertTrue(list.contains(obj)); + + // (4) subList(int org0, int arg1) + List subList = list.subList(0, RESULTS_NUM); + assertEquals(RESULTS_NUM, subList.size()); + try { + list.subList(0, RESULTS_NUM + 1); + fail("IndexOutOfBoundsException is IndexOutOfBoundsException but not thrown"); + } catch (IndexOutOfBoundsException e) {} + + // (5) indexOf(Object org0) + assertTrue(list.indexOf(obj) < RESULTS_NUM); + + // (6) loadAllResults() + list.loadAllResults(); + + // (7) size() + assertEquals(RESULTS_NUM, list.size()); + + } + + private static void testPaginatedListIterator(PaginatedList list) { + for (RangeKeyClass item : list) { + assertEquals(hashKey, item.getKey()); + assertTrue(item.getRangeKey() < OBJECTS_NUM); + } + + // make sure the list could be iterated again + for (RangeKeyClass item : list) { + assertEquals(hashKey, item.getKey()); + assertTrue(item.getRangeKey() < OBJECTS_NUM); + } + } + + private static void testIterationOnlyPaginatedListOperations(PaginatedList list) { + + // Unsupported operations + + // (1) isEmpty() + try { + list.isEmpty(); + fail("UnsupportedOperationException expected but is not thrown"); + } catch (UnsupportedOperationException e) {} + + // (2) get(int n) + try { + list.get(RESULTS_NUM / 2); + fail("UnsupportedOperationException expected but is not thrown"); + } catch (UnsupportedOperationException e) {} + + // (3) contains(Object org0) + try { + list.contains(new RangeKeyClass()); + fail("UnsupportedOperationException expected but is not thrown"); + } catch (UnsupportedOperationException e) {} + + // (4) subList(int org0, int arg1) + try { + list.subList(0, RESULTS_NUM); + fail("UnsupportedOperationException expected but is not thrown"); + } catch (UnsupportedOperationException e) {} + + // (5) indexOf(Object org0) + try { + list.indexOf(new RangeKeyClass()); + fail("UnsupportedOperationException expected but is not thrown"); + } catch (UnsupportedOperationException e) {} + + // (6) loadAllResults() + try { + list.loadAllResults(); + fail("UnsupportedOperationException expected but is not thrown"); + } catch (UnsupportedOperationException e) {} + + // (7) size() + try { + list.size(); + fail("UnsupportedOperationException expected but is not thrown"); + } catch (UnsupportedOperationException e) {}; + + // Could be iterated once + for (RangeKeyClass item : list) { + assertEquals(hashKey, item.getKey()); + assertTrue(item.getRangeKey() < OBJECTS_NUM); + // At most one page of results in memeory + assertTrue(getLoadedResultsNumber(list) <= PAGE_SIZE); + } + + // not twice + try { + for (@SuppressWarnings("unused") RangeKeyClass item : list) { + fail("UnsupportedOperationException expected but is not thrown"); + } + } catch (UnsupportedOperationException e) {} + + } + + /** Use reflection to get the size of the private allResults field **/ + @SuppressWarnings("unchecked") + private static int getLoadedResultsNumber(PaginatedList list) { + Field privateAllResults = null; + try { + privateAllResults = list.getClass().getSuperclass().getDeclaredField("allResults"); + } catch (SecurityException e) { + fail(e.getMessage()); + } catch (NoSuchFieldException e) { + fail(e.getMessage()); + } + privateAllResults.setAccessible(true); + List allResults = null; + try { + allResults = (List) privateAllResults.get(list); + } catch (IllegalArgumentException e) { + fail(e.getMessage()); + } catch (IllegalAccessException e) { + fail(e.getMessage()); + } + return allResults.size(); + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/MapperQueryExpressionTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/MapperQueryExpressionTest.java new file mode 100644 index 000000000000..8bfe81277b69 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/MapperQueryExpressionTest.java @@ -0,0 +1,543 @@ +/* + * 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.mapper.dynamodb.mapper; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBRangeKey; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBIndexHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBIndexRangeKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBQueryExpression; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.ComparisonOperator; +import software.amazon.awssdk.services.dynamodb.model.Condition; +import software.amazon.awssdk.services.dynamodb.model.QueryRequest; +import software.amazon.awssdk.services.dynamodb.model.QueryResponse; + + +/** + * Unit test for the private method DynamoDBMapper#createQueryRequestFromExpression + */ +public class MapperQueryExpressionTest { + + private static final String TABLE_NAME = "table_name"; + private static final Condition RANGE_KEY_CONDITION = Condition.builder() + .attributeValueList(AttributeValue.builder().s("some value").build()) + .comparisonOperator(ComparisonOperator.EQ) + .build(); + + private static DynamoDbClient mockDynamoDb; + private static DynamoDBMapper mapper; + + @BeforeClass + public static void setUp() { + mockDynamoDb = mock(DynamoDbClient.class); + when(mockDynamoDb.query(any(QueryRequest.class))) + .thenReturn(QueryResponse.builder().items(Collections.emptyList()).build()); + mapper = new DynamoDBMapper(mockDynamoDb); + } + + @DynamoDBTable(tableName = TABLE_NAME) + public final class HashOnlyClass { + + @DynamoDBHashKey + @DynamoDBIndexHashKey ( + globalSecondaryIndexNames = "GSI-primary-hash" + ) + private String primaryHashKey; + + @DynamoDBIndexHashKey ( + globalSecondaryIndexNames = {"GSI-index-hash-1", "GSI-index-hash-2"} + ) + private String indexHashKey; + + @DynamoDBIndexHashKey ( + globalSecondaryIndexNames = {"GSI-another-index-hash"} + ) + private String anotherIndexHashKey; + + public HashOnlyClass(String primaryHashKey, String indexHashKey, String anotherIndexHashKey) { + this.primaryHashKey = primaryHashKey; + this.indexHashKey = indexHashKey; + this.anotherIndexHashKey = anotherIndexHashKey; + } + + public String getPrimaryHashKey() { + return primaryHashKey; + } + + public void setPrimaryHashKey(String primaryHashKey) { + this.primaryHashKey = primaryHashKey; + } + + public String getIndexHashKey() { + return indexHashKey; + } + + public void setIndexHashKey(String indexHashKey) { + this.indexHashKey = indexHashKey; + } + + public String getAnotherIndexHashKey() { + return anotherIndexHashKey; + } + + public void setAnotherIndexHashKey(String anotherIndexHashKey) { + this.anotherIndexHashKey = anotherIndexHashKey; + } + } + + /** Tests different scenarios of hash-only query **/ + @Test + public void testHashConditionOnly() { + // Primary hash only + QueryRequest queryRequest = testCreateQueryRequestFromExpression( + HashOnlyClass.class, + new DynamoDBQueryExpression() + .withHashKeyValues(new HashOnlyClass("foo", null, null))); + assertTrue(queryRequest.keyConditions().size() == 1); + assertEquals("primaryHashKey", queryRequest.keyConditions().keySet().iterator().next()); + assertEquals( + Condition.builder().attributeValueList(AttributeValue.builder().s("foo").build()).comparisonOperator(ComparisonOperator.EQ).build(), + queryRequest.keyConditions().get("primaryHashKey")); + assertNull(queryRequest.indexName()); + + // Primary hash used for a GSI + queryRequest = testCreateQueryRequestFromExpression( + HashOnlyClass.class, + new DynamoDBQueryExpression() + .withHashKeyValues(new HashOnlyClass("foo", null, null)) + .withIndexName("GSI-primary-hash")); + assertTrue(queryRequest.keyConditions().size() == 1); + assertEquals("primaryHashKey", queryRequest.keyConditions().keySet().iterator().next()); + assertEquals( + Condition.builder().attributeValueList(AttributeValue.builder().s("foo").build()).comparisonOperator(ComparisonOperator.EQ).build(), + queryRequest.keyConditions().get("primaryHashKey")); + assertEquals("GSI-primary-hash", queryRequest.indexName()); + + // Primary hash query takes higher priority then index hash query + queryRequest = testCreateQueryRequestFromExpression( + HashOnlyClass.class, + new DynamoDBQueryExpression() + .withHashKeyValues(new HashOnlyClass("foo", "bar", null))); + assertTrue(queryRequest.keyConditions().size() == 1); + assertEquals("primaryHashKey", queryRequest.keyConditions().keySet().iterator().next()); + assertEquals( + Condition.builder().attributeValueList(AttributeValue.builder().s("foo").build()).comparisonOperator(ComparisonOperator.EQ).build(), + queryRequest.keyConditions().get("primaryHashKey")); + assertNull(queryRequest.indexName()); + + // Ambiguous query on multiple index hash keys + queryRequest = testCreateQueryRequestFromExpression( + HashOnlyClass.class, + new DynamoDBQueryExpression() + .withHashKeyValues(new HashOnlyClass(null, "bar", "charlie")), + "Ambiguous query expression: More than one index hash key EQ conditions"); + + // Ambiguous query when not specifying index name + queryRequest = testCreateQueryRequestFromExpression( + HashOnlyClass.class, + new DynamoDBQueryExpression() + .withHashKeyValues(new HashOnlyClass(null, "bar", null)), + "Ambiguous query expression: More than one GSIs"); + + // Explicitly specify a GSI. + queryRequest = testCreateQueryRequestFromExpression( + HashOnlyClass.class, + new DynamoDBQueryExpression() + .withHashKeyValues(new HashOnlyClass("foo", "bar", null)) + .withIndexName("GSI-index-hash-1")); + assertTrue(queryRequest.keyConditions().size() == 1); + assertEquals("indexHashKey", queryRequest.keyConditions().keySet().iterator().next()); + assertEquals( + Condition.builder().attributeValueList(AttributeValue.builder().s("bar").build()).comparisonOperator(ComparisonOperator.EQ).build(), + queryRequest.keyConditions().get("indexHashKey")); + assertEquals("GSI-index-hash-1", queryRequest.indexName()); + + // Non-existent GSI + queryRequest = testCreateQueryRequestFromExpression( + HashOnlyClass.class, + new DynamoDBQueryExpression() + .withHashKeyValues(new HashOnlyClass("foo", "bar", null)) + .withIndexName("some fake gsi"), + "No hash key condition is applicable to the specified index"); + + // No hash key condition specified + queryRequest = testCreateQueryRequestFromExpression( + HashOnlyClass.class, + new DynamoDBQueryExpression() + .withHashKeyValues(new HashOnlyClass(null, null, null)), + "Illegal query expression: No hash key condition is found in the query"); + } + + @DynamoDBTable(tableName = TABLE_NAME) + public final class HashRangeClass { + private String primaryHashKey; + private String indexHashKey; + private String primaryRangeKey; + private String indexRangeKey; + private String anotherIndexRangeKey; + + public HashRangeClass(String primaryHashKey, String indexHashKey) { + this.primaryHashKey = primaryHashKey; + this.indexHashKey = indexHashKey; + } + + @DynamoDBHashKey + @DynamoDBIndexHashKey ( + globalSecondaryIndexNames = { + "GSI-primary-hash-index-range-1", + "GSI-primary-hash-index-range-2"} + ) + public String getPrimaryHashKey() { + return primaryHashKey; + } + + public void setPrimaryHashKey(String primaryHashKey) { + this.primaryHashKey = primaryHashKey; + } + + @DynamoDBIndexHashKey ( + globalSecondaryIndexNames = { + "GSI-index-hash-primary-range", + "GSI-index-hash-index-range-1", + "GSI-index-hash-index-range-2"} + ) + public String getIndexHashKey() { + return indexHashKey; + } + + public void setIndexHashKey(String indexHashKey) { + this.indexHashKey = indexHashKey; + } + + @DynamoDBRangeKey + @DynamoDBIndexRangeKey ( + globalSecondaryIndexNames = {"GSI-index-hash-primary-range"}, + localSecondaryIndexName = "LSI-primary-range" + ) + public String getPrimaryRangeKey() { + return primaryRangeKey; + } + + public void setPrimaryRangeKey(String primaryRangeKey) { + this.primaryRangeKey = primaryRangeKey; + } + + @DynamoDBIndexRangeKey ( + globalSecondaryIndexNames = { + "GSI-primary-hash-index-range-1", + "GSI-index-hash-index-range-1", + "GSI-index-hash-index-range-2"}, + localSecondaryIndexNames = {"LSI-index-range-1", "LSI-index-range-2"} + ) + public String getIndexRangeKey() { + return indexRangeKey; + } + + public void setIndexRangeKey(String indexRangeKey) { + this.indexRangeKey = indexRangeKey; + } + + @DynamoDBIndexRangeKey ( + localSecondaryIndexName = "LSI-index-range-3", + globalSecondaryIndexName = "GSI-primary-hash-index-range-2" + ) + public String getAnotherIndexRangeKey() { + return anotherIndexRangeKey; + } + + public void setAnotherIndexRangeKey(String anotherIndexRangeKey) { + this.anotherIndexRangeKey = anotherIndexRangeKey; + } + } + + /** Tests hash + range query **/ + @Test + public void testHashAndRangeCondition() { + // Primary hash + primary range + QueryRequest queryRequest = testCreateQueryRequestFromExpression( + HashRangeClass.class, + new DynamoDBQueryExpression() + .withHashKeyValues(new HashRangeClass("foo", null)) + .withRangeKeyCondition("primaryRangeKey", RANGE_KEY_CONDITION)); + assertTrue(queryRequest.keyConditions().size() == 2); + assertTrue(queryRequest.keyConditions().containsKey("primaryHashKey")); + assertEquals( + Condition.builder().attributeValueList(AttributeValue.builder().s("foo").build()).comparisonOperator(ComparisonOperator.EQ).build(), + queryRequest.keyConditions().get("primaryHashKey")); + assertTrue(queryRequest.keyConditions().containsKey("primaryRangeKey")); + assertEquals(RANGE_KEY_CONDITION, queryRequest.keyConditions().get("primaryRangeKey")); + assertNull(queryRequest.indexName()); + + // Primary hash + primary range on a LSI + queryRequest = testCreateQueryRequestFromExpression( + HashRangeClass.class, + new DynamoDBQueryExpression() + .withHashKeyValues(new HashRangeClass("foo", null)) + .withRangeKeyCondition("primaryRangeKey", RANGE_KEY_CONDITION) + .withIndexName("LSI-primary-range")); + assertTrue(queryRequest.keyConditions().size() == 2); + assertTrue(queryRequest.keyConditions().containsKey("primaryHashKey")); + assertEquals( + Condition.builder().attributeValueList(AttributeValue.builder().s("foo").build()).comparisonOperator(ComparisonOperator.EQ).build(), + queryRequest.keyConditions().get("primaryHashKey")); + assertTrue(queryRequest.keyConditions().containsKey("primaryRangeKey")); + assertEquals(RANGE_KEY_CONDITION, queryRequest.keyConditions().get("primaryRangeKey")); + assertEquals("LSI-primary-range", queryRequest.indexName()); + + // Primary hash + index range used by multiple LSI. But also a GSI hash + range + queryRequest = testCreateQueryRequestFromExpression( + HashRangeClass.class, + new DynamoDBQueryExpression() + .withHashKeyValues(new HashRangeClass("foo", null)) + .withRangeKeyCondition("indexRangeKey", RANGE_KEY_CONDITION)); + assertTrue(queryRequest.keyConditions().size() == 2); + assertTrue(queryRequest.keyConditions().containsKey("primaryHashKey")); + assertEquals( + Condition.builder().attributeValueList(AttributeValue.builder().s("foo").build()).comparisonOperator(ComparisonOperator.EQ).build(), + queryRequest.keyConditions().get("primaryHashKey")); + assertTrue(queryRequest.keyConditions().containsKey("indexRangeKey")); + assertEquals(RANGE_KEY_CONDITION, queryRequest.keyConditions().get("indexRangeKey")); + assertEquals("GSI-primary-hash-index-range-1", queryRequest.indexName()); + + + // Primary hash + index range on a LSI + queryRequest = testCreateQueryRequestFromExpression( + HashRangeClass.class, + new DynamoDBQueryExpression() + .withHashKeyValues(new HashRangeClass("foo", null)) + .withRangeKeyCondition("indexRangeKey", RANGE_KEY_CONDITION) + .withIndexName("LSI-index-range-1")); + assertTrue(queryRequest.keyConditions().size() == 2); + assertTrue(queryRequest.keyConditions().containsKey("primaryHashKey")); + assertEquals( + Condition.builder().attributeValueList(AttributeValue.builder().s("foo").build()).comparisonOperator(ComparisonOperator.EQ).build(), + queryRequest.keyConditions().get("primaryHashKey")); + assertTrue(queryRequest.keyConditions().containsKey("indexRangeKey")); + assertEquals(RANGE_KEY_CONDITION, queryRequest.keyConditions().get("indexRangeKey")); + assertEquals("LSI-index-range-1", queryRequest.indexName()); + + // Non-existent LSI + queryRequest = testCreateQueryRequestFromExpression( + HashRangeClass.class, + new DynamoDBQueryExpression() + .withHashKeyValues(new HashRangeClass("foo", null)) + .withRangeKeyCondition("indexRangeKey", RANGE_KEY_CONDITION) + .withIndexName("some fake lsi"), + "No range key condition is applicable to the specified index"); + + // Illegal query: Primary hash + primary range on a GSI + queryRequest = testCreateQueryRequestFromExpression( + HashRangeClass.class, + new DynamoDBQueryExpression() + .withHashKeyValues(new HashRangeClass("foo", null)) + .withRangeKeyCondition("indexRangeKey", RANGE_KEY_CONDITION) + .withIndexName("GSI-index-hash-index-range-1"), + "Illegal query expression: No hash key condition is applicable to the specified index"); + + // GSI hash + GSI range + queryRequest = testCreateQueryRequestFromExpression( + HashRangeClass.class, + new DynamoDBQueryExpression() + .withHashKeyValues(new HashRangeClass(null, "foo")) + .withRangeKeyCondition("primaryRangeKey", RANGE_KEY_CONDITION)); + assertTrue(queryRequest.keyConditions().size() == 2); + assertTrue(queryRequest.keyConditions().containsKey("indexHashKey")); + assertEquals( + Condition.builder().attributeValueList(AttributeValue.builder().s("foo").build()).comparisonOperator(ComparisonOperator.EQ).build(), + queryRequest.keyConditions().get("indexHashKey")); + assertTrue(queryRequest.keyConditions().containsKey("primaryRangeKey")); + assertEquals(RANGE_KEY_CONDITION, queryRequest.keyConditions().get("primaryRangeKey")); + assertEquals("GSI-index-hash-primary-range", queryRequest.indexName()); + + // Ambiguous query: GSI hash + index range used by multiple GSIs + queryRequest = testCreateQueryRequestFromExpression( + HashRangeClass.class, + new DynamoDBQueryExpression() + .withHashKeyValues(new HashRangeClass(null, "foo")) + .withRangeKeyCondition("indexRangeKey", RANGE_KEY_CONDITION), + "Illegal query expression: Cannot infer the index name from the query expression."); + + // Explicitly specify the GSI name + queryRequest = testCreateQueryRequestFromExpression( + HashRangeClass.class, + new DynamoDBQueryExpression() + .withHashKeyValues(new HashRangeClass(null, "foo")) + .withRangeKeyCondition("indexRangeKey", RANGE_KEY_CONDITION) + .withIndexName("GSI-index-hash-index-range-2")); + assertTrue(queryRequest.keyConditions().size() == 2); + assertTrue(queryRequest.keyConditions().containsKey("indexHashKey")); + assertEquals( + Condition.builder().attributeValueList(AttributeValue.builder().s("foo").build()).comparisonOperator(ComparisonOperator.EQ).build(), + queryRequest.keyConditions().get("indexHashKey")); + assertTrue(queryRequest.keyConditions().containsKey("indexRangeKey")); + assertEquals(RANGE_KEY_CONDITION, queryRequest.keyConditions().get("indexRangeKey")); + assertEquals("GSI-index-hash-index-range-2", queryRequest.indexName()); + + // Ambiguous query: (1) primary hash + LSI range OR (2) GSI hash + range + queryRequest = testCreateQueryRequestFromExpression( + HashRangeClass.class, + new DynamoDBQueryExpression() + .withHashKeyValues(new HashRangeClass("foo", null)) + .withRangeKeyCondition("anotherIndexRangeKey", RANGE_KEY_CONDITION), + "Ambiguous query expression: Found multiple valid queries:"); + + // Multiple range key conditions specified + Map multipleRangeKeyConditions = new HashMap<>(); + multipleRangeKeyConditions.put("primaryRangeKey", RANGE_KEY_CONDITION); + multipleRangeKeyConditions.put("indexRangeKey", RANGE_KEY_CONDITION); + queryRequest = testCreateQueryRequestFromExpression( + HashRangeClass.class, + new DynamoDBQueryExpression() + .withHashKeyValues(new HashRangeClass("foo", null)) + .withRangeKeyConditions(multipleRangeKeyConditions), + "Illegal query expression: Conditions on multiple range keys"); + + // Using an un-annotated range key + queryRequest = testCreateQueryRequestFromExpression( + HashRangeClass.class, + new DynamoDBQueryExpression() + .withHashKeyValues(new HashRangeClass("foo", null)) + .withRangeKeyCondition("indexHashKey", RANGE_KEY_CONDITION), + "not annotated with either @DynamoDBRangeKey or @DynamoDBIndexRangeKey."); + } + + + @DynamoDBTable(tableName = TABLE_NAME) + public final class LSIRangeKeyClass { + private String primaryHashKey; + private String primaryRangeKey; + private String lsiRangeKey; + + public LSIRangeKeyClass(String primaryHashKey, String primaryRangeKey) { + this.primaryHashKey = primaryHashKey; + this.primaryRangeKey = primaryRangeKey; + } + + @DynamoDBHashKey + public String getPrimaryHashKey() { + return primaryHashKey; + } + public void setPrimaryHashKey(String primaryHashKey) { + this.primaryHashKey = primaryHashKey; + } + + @DynamoDBRangeKey + public String getPrimaryRangeKey() { + return primaryRangeKey; + } + public void setPrimaryRangeKey(String primaryRangeKey) { + this.primaryRangeKey = primaryRangeKey; + } + + @DynamoDBIndexRangeKey(localSecondaryIndexName = "LSI") + public String getLsiRangeKey() { + return lsiRangeKey; + } + public void setLsiRangeKey(String lsiRangeKey) { + this.lsiRangeKey = lsiRangeKey; + } + } + + @Test + public void testHashOnlyQueryOnHashRangeTable() { + // Primary hash only query on a Hash+Range table + QueryRequest queryRequest = testCreateQueryRequestFromExpression( + LSIRangeKeyClass.class, + new DynamoDBQueryExpression() + .withHashKeyValues(new LSIRangeKeyClass("foo", null))); + assertTrue(queryRequest.keyConditions().size() == 1); + assertTrue(queryRequest.keyConditions().containsKey("primaryHashKey")); + assertNull(queryRequest.indexName()); + + // Hash+Range query on a LSI + queryRequest = testCreateQueryRequestFromExpression( + LSIRangeKeyClass.class, + new DynamoDBQueryExpression() + .withHashKeyValues(new LSIRangeKeyClass("foo", null)) + .withRangeKeyCondition("lsiRangeKey", RANGE_KEY_CONDITION) + .withIndexName("LSI")); + assertTrue(queryRequest.keyConditions().size() == 2); + assertTrue(queryRequest.keyConditions().containsKey("primaryHashKey")); + assertTrue(queryRequest.keyConditions().containsKey("lsiRangeKey")); + assertEquals("LSI", queryRequest.indexName()); + + // Hash-only query on a LSI + queryRequest = testCreateQueryRequestFromExpression( + LSIRangeKeyClass.class, + new DynamoDBQueryExpression() + .withHashKeyValues(new LSIRangeKeyClass("foo", null)) + .withIndexName("LSI")); + assertTrue(queryRequest.keyConditions().size() == 1); + assertTrue(queryRequest.keyConditions().containsKey("primaryHashKey")); + assertEquals("LSI", queryRequest.indexName()); + } + + private static QueryRequest testCreateQueryRequestFromExpression( + Class clazz, DynamoDBQueryExpression queryExpression) { + return testCreateQueryRequestFromExpression(clazz, queryExpression, null); + } + + private static QueryRequest testCreateQueryRequestFromExpression( + Class clazz, DynamoDBQueryExpression queryExpression, + String expectedErrorMessage) { + try { + mapper.queryPage(clazz, queryExpression, DynamoDBMapperConfig.DEFAULT); + if (expectedErrorMessage != null) { + fail("Exception containing messsage (" + + expectedErrorMessage + ") is expected."); + } + ArgumentCaptor captor = ArgumentCaptor.forClass(QueryRequest.class); + verify(mockDynamoDb, atLeastOnce()).query(captor.capture()); + return captor.getValue(); + } catch (RuntimeException e) { + if (expectedErrorMessage != null && e.getMessage() != null) { + assertTrue("Exception message [" + e.getMessage() + "] does not contain " + + "the expected message [" + expectedErrorMessage + "].", + e.getMessage().contains(expectedErrorMessage)); + } else { + e.printStackTrace(); + fail("Internal error when calling createQueryRequestFromExpressio method"); + } + } catch (Exception e) { + fail(e.getMessage()); + } + return null; + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/MapperSaveConfigIntegrationTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/MapperSaveConfigIntegrationTest.java new file mode 100644 index 000000000000..f4005ea360a7 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/MapperSaveConfigIntegrationTest.java @@ -0,0 +1,679 @@ +/* + * 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.mapper.dynamodb.mapper; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException; +import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; +import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; +import software.amazon.awssdk.mapper.dynamodb.pojos.TestItem; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import org.junit.AfterClass; +import org.junit.Test; + +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; + +/** + * Tests the behavior of save method of DynamoDBMapper under different + * SaveBehavior configurations. + */ +public class MapperSaveConfigIntegrationTest extends MapperSaveConfigTestBase { + + @AfterClass + public static void teatDown() throws Exception { + try { +// dynamo.deleteTable(new DeleteTableRequest(tableName)); + } catch (Exception e) { + } + } + + /********************************************* + ** UPDATE (default) ** + *********************************************/ + + /** + * Tests that a key-only object could be saved with + * UPDATE configuration, even when the key has already existed in the table. + */ + @Test + public void testDefaultWithOnlyKeyAttributesSpecifiedRecordInTable() + throws Exception { + + /* First put a new item (with non-key attribute)*/ + TestItem testItem = putRandomUniqueItem("foo", null); + + /* Put an key-only object with the same key */ + testItem.setNonKeyAttribute(null); + + dynamoMapper.save(testItem, defaultConfig); + + /* The non-key attribute should be nulled out. */ + TestItem returnedObject = (TestItem) dynamoMapper.load(testItem); + assertNotNull(returnedObject); + assertEquals(testItem.getHashKey(), returnedObject.getHashKey()); + assertEquals(testItem.getRangeKey(), returnedObject.getRangeKey()); + assertNull(returnedObject.getNonKeyAttribute()); + } + + /** + * Tests an edge case that we have fixed according a forum bug report. If + * the object is only specified with key attributes, and such key is not + * present in the table, we should add this object by a key-only put + * request even if it is using UPDATE configuration. + */ + @Test + public void testDefaultWithOnlyKeyAttributesSpecifiedRecordNotInTable() + throws Exception { + TestItem testItem = new TestItem(); + testItem.setHashKey(UUID.randomUUID().toString()); + testItem.setRangeKey(System.currentTimeMillis()); + + dynamoMapper.save(testItem, defaultConfig); + + TestItem returnedObject = (TestItem) dynamoMapper.load(testItem); + + assertNotNull(returnedObject); + assertEquals(testItem.getHashKey(), returnedObject.getHashKey()); + assertEquals(testItem.getRangeKey(), returnedObject.getRangeKey()); + assertNull(returnedObject.getNonKeyAttribute()); + } + + /** + * Update an existing item in the table. + */ + @Test + public void testDefaultWithKeyAndNonKeyAttributesSpecifiedRecordInTable() + throws Exception { + + /* First put a new item (without non-key attribute)*/ + TestItem testItem = putRandomUniqueItem(null, null); + String hashKeyValue = testItem.getHashKey(); + Long rangeKeyValue = testItem.getRangeKey(); + + TestItem returnedObject = (TestItem) dynamoMapper.load(testItem); + + assertNotNull(returnedObject); + assertEquals(hashKeyValue, returnedObject.getHashKey()); + assertEquals(rangeKeyValue, returnedObject.getRangeKey()); + assertNull(returnedObject.getNonKeyAttribute()); + + /* Put an updated object with the same key and an additional non-key attribute. */ + testItem.setHashKey(hashKeyValue); + testItem.setRangeKey(rangeKeyValue); + testItem.setNonKeyAttribute("update"); + + dynamoMapper.save(testItem, defaultConfig); + returnedObject = (TestItem) dynamoMapper.load(testItem); + + assertNotNull(returnedObject); + assertEquals(testItem.getHashKey(), returnedObject.getHashKey()); + assertEquals(testItem.getRangeKey(), returnedObject.getRangeKey()); + assertEquals(testItem.getNonKeyAttribute(), returnedObject.getNonKeyAttribute()); + } + + /** + * Use UPDATE to put a new item in the table. + */ + @Test + public void testDefaultWithKeyAndNonKeyAttributesSpecifiedRecordNotInTable() + throws Exception { + TestItem testItem = new TestItem(); + testItem.setHashKey(UUID.randomUUID().toString()); + testItem.setRangeKey(System.currentTimeMillis()); + testItem.setNonKeyAttribute("new item"); + + dynamoMapper.save(testItem, defaultConfig); + + TestItem returnedObject = (TestItem) dynamoMapper.load(testItem); + + assertNotNull(returnedObject); + assertEquals(testItem.getHashKey(), returnedObject.getHashKey()); + assertEquals(testItem.getRangeKey(), returnedObject.getRangeKey()); + assertEquals(testItem.getNonKeyAttribute(), returnedObject.getNonKeyAttribute()); + } + + /********************************************* + ** UPDATE_SKIP_NULL_ATTRIBUTES ** + *********************************************/ + + /** + * When using UPDATE_SKIP_NULL_ATTRIBUTES, key-only update on existing item + * should not affect the item at all, since all the null-valued non-key + * attributes are ignored. + */ + @Test + public void testUpdateSkipNullWithOnlyKeyAttributesSpecifiedRecordInTable() + throws Exception { + + /* First put a new item (with non-key attribute)*/ + TestItem testItem = putRandomUniqueItem("foo", null); + + /* Put an key-only object with the same key */ + testItem.setNonKeyAttribute(null); + + dynamoMapper.save(testItem, updateSkipNullConfig); + + TestItem returnedObject = (TestItem) dynamoMapper.load(testItem); + + /* The non-key attribute should not be removed */ + assertNotNull(returnedObject); + assertEquals(testItem.getHashKey(), returnedObject.getHashKey()); + assertEquals(testItem.getRangeKey(), returnedObject.getRangeKey()); + assertEquals("foo", returnedObject.getNonKeyAttribute()); + } + + /** + * The behavior should be the same as UPDATE. + */ + @Test + public void testUpdateSkipNullWithOnlyKeyAttributesSpecifiedRecordNotInTable() + throws Exception { + TestItem testItem = new TestItem(); + testItem.setHashKey(UUID.randomUUID().toString()); + testItem.setRangeKey(System.currentTimeMillis()); + + dynamoMapper.save(testItem, updateSkipNullConfig); + + TestItem returnedObject = (TestItem) dynamoMapper.load(testItem); + + assertNotNull(returnedObject); + assertEquals(testItem.getHashKey(), returnedObject.getHashKey()); + assertEquals(testItem.getRangeKey(), returnedObject.getRangeKey()); + assertNull(returnedObject.getNonKeyAttribute()); + } + + /** + * Use UPDATE_SKIP_NULL_ATTRIBUTES to update an existing item in the table. + */ + @Test + public void testUpdateSkipNullWithKeyAndNonKeyAttributesSpecifiedRecordInTable() + throws Exception { + + /* First put a new item (without non-key attribute)*/ + TestItem testItem = putRandomUniqueItem(null, null); + String hashKeyValue = testItem.getHashKey(); + Long rangeKeyValue = testItem.getRangeKey(); + + TestItem returnedObject = (TestItem) dynamoMapper.load(testItem); + + assertNotNull(returnedObject); + assertEquals(hashKeyValue, returnedObject.getHashKey()); + assertEquals(rangeKeyValue, returnedObject.getRangeKey()); + assertNull(returnedObject.getNonKeyAttribute()); + + /* Put an updated object with the same key and an additional non-key attribute. */ + String nonKeyAttributeValue = "update"; + testItem.setHashKey(hashKeyValue); + testItem.setRangeKey(rangeKeyValue); + testItem.setNonKeyAttribute(nonKeyAttributeValue); + + dynamoMapper.save(testItem, updateSkipNullConfig); + returnedObject = (TestItem) dynamoMapper.load(testItem); + + assertNotNull(returnedObject); + assertEquals(testItem.getHashKey(), returnedObject.getHashKey()); + assertEquals(testItem.getRangeKey(), returnedObject.getRangeKey()); + assertEquals(testItem.getNonKeyAttribute(), returnedObject.getNonKeyAttribute()); + + /* At last, save the object again, but with non-key attribute set as null. + * This should not change the existing item. + */ + testItem.setNonKeyAttribute(null); + dynamoMapper.save(testItem, updateSkipNullConfig); + returnedObject = (TestItem) dynamoMapper.load(testItem); + + assertNotNull(returnedObject); + assertEquals(testItem.getHashKey(), returnedObject.getHashKey()); + assertEquals(testItem.getRangeKey(), returnedObject.getRangeKey()); + assertEquals(nonKeyAttributeValue, returnedObject.getNonKeyAttribute()); + } + + /** + * Use UPDATE_SKIP_NULL_ATTRIBUTES to put a new item in the table. + */ + @Test + public void testUpdateSkipNullWithKeyAndNonKeyAttributesSpecifiedRecordNotInTable() + throws Exception { + TestItem testItem = new TestItem(); + testItem.setHashKey(UUID.randomUUID().toString()); + testItem.setRangeKey(System.currentTimeMillis()); + testItem.setNonKeyAttribute("new item"); + + dynamoMapper.save(testItem, updateSkipNullConfig); + + TestItem returnedObject = (TestItem) dynamoMapper.load(testItem); + + assertNotNull(returnedObject); + assertEquals(testItem.getHashKey(), returnedObject.getHashKey()); + assertEquals(testItem.getRangeKey(), returnedObject.getRangeKey()); + assertEquals(testItem.getNonKeyAttribute(), returnedObject.getNonKeyAttribute()); + } + + /********************************************* + ** APPEND_SET ** + *********************************************/ + + /** + * The behavior should be the same as UPDATE_SKIP_NULL_ATTRIBUTES. + */ + @Test + public void testAppendSetWithOnlyKeyAttributesSpecifiedRecordInTable() + throws Exception { + + /* First put a new item (with non-key attributes)*/ + Set randomSet = generateRandomStringSet(3); + TestItem testItem = putRandomUniqueItem("foo", randomSet); + + /* Put an key-only object with the same key */ + testItem.setNonKeyAttribute(null); + testItem.setStringSetAttribute(null); + + dynamoMapper.save(testItem, appendSetConfig); + + TestItem returnedObject = (TestItem) dynamoMapper.load(testItem); + + /* The non-key attribute should not be removed */ + assertNotNull(returnedObject); + assertEquals(testItem.getHashKey(), returnedObject.getHashKey()); + assertEquals(testItem.getRangeKey(), returnedObject.getRangeKey()); + assertEquals("foo", returnedObject.getNonKeyAttribute()); + assertTrue(assertSetEquals(randomSet, returnedObject.getStringSetAttribute())); + } + + /** + * The behavior should be the same as UPDATE and UPDATE_SKIP_NULL_ATTRIBUTES. + */ + @Test + public void testAppendSetWithOnlyKeyAttributesSpecifiedRecordNotInTable() + throws Exception { + TestItem testItem = new TestItem(); + testItem.setHashKey(UUID.randomUUID().toString()); + testItem.setRangeKey(System.currentTimeMillis()); + + dynamoMapper.save(testItem, appendSetConfig); + + TestItem returnedObject = (TestItem) dynamoMapper.load(testItem); + + assertNotNull(returnedObject); + assertEquals(testItem.getHashKey(), returnedObject.getHashKey()); + assertEquals(testItem.getRangeKey(), returnedObject.getRangeKey()); + assertNull(returnedObject.getNonKeyAttribute()); + assertNull(returnedObject.getStringSetAttribute()); + } + + /** + * Use APPEND_SET to update an existing item in the table. + */ + @Test + public void testAppendSetWithKeyAndNonKeyAttributesSpecifiedRecordInTable() + throws Exception { + + /* First put a new item (without non-key attribute)*/ + TestItem testItem = putRandomUniqueItem(null, null); + String hashKeyValue = testItem.getHashKey(); + Long rangeKeyValue = testItem.getRangeKey(); + + TestItem returnedObject = (TestItem) dynamoMapper.load(testItem); + + assertNotNull(returnedObject); + assertEquals(hashKeyValue, returnedObject.getHashKey()); + assertEquals(rangeKeyValue, returnedObject.getRangeKey()); + assertNull(returnedObject.getNonKeyAttribute()); + assertNull(returnedObject.getStringSetAttribute()); + + /* Put an updated object with the same key and an additional non-key attribute. */ + String nonKeyAttributeValue = "update"; + Set stringSetAttributeValue = generateRandomStringSet(3); + testItem.setHashKey(hashKeyValue); + testItem.setRangeKey(rangeKeyValue); + testItem.setNonKeyAttribute(nonKeyAttributeValue); + testItem.setStringSetAttribute(stringSetAttributeValue); + + dynamoMapper.save(testItem, appendSetConfig); + returnedObject = (TestItem) dynamoMapper.load(testItem); + + assertNotNull(returnedObject); + assertEquals(testItem.getHashKey(), returnedObject.getHashKey()); + assertEquals(testItem.getRangeKey(), returnedObject.getRangeKey()); + assertEquals(testItem.getNonKeyAttribute(), returnedObject.getNonKeyAttribute()); + assertTrue(assertSetEquals(testItem.getStringSetAttribute(), returnedObject.getStringSetAttribute())); + + /* Override nonKeyAttribute and append stringSetAttribute */ + testItem.setNonKeyAttribute("blabla"); + Set appendSetAttribute = generateRandomStringSet(3); + testItem.setStringSetAttribute(appendSetAttribute); + dynamoMapper.save(testItem, appendSetConfig); + returnedObject = (TestItem) dynamoMapper.load(testItem); + + assertNotNull(returnedObject); + assertEquals(testItem.getHashKey(), returnedObject.getHashKey()); + assertEquals(testItem.getRangeKey(), returnedObject.getRangeKey()); + assertEquals("blabla", returnedObject.getNonKeyAttribute()); + // expected set after the append + stringSetAttributeValue.addAll(appendSetAttribute); + assertTrue(assertSetEquals(stringSetAttributeValue, returnedObject.getStringSetAttribute())); + + /* Append on an existing scalar attribute would result in an exception */ + TestAppendToScalarItem testAppendToScalarItem = new TestAppendToScalarItem(); + testAppendToScalarItem.setHashKey(testItem.getHashKey()); + testAppendToScalarItem.setRangeKey(testItem.getRangeKey()); + // this fake set attribute actually points to a scalar attribute + testAppendToScalarItem.setFakeStringSetAttribute(generateRandomStringSet(1)); + try { + dynamoMapper.save(testAppendToScalarItem, appendSetConfig); + fail("Should have thrown a 'Type mismatch' service exception."); + } catch (AwsServiceException ase) { + assertEquals("ValidationException", ase.awsErrorDetails().errorCode()); + } + } + + /** + * Use APPEND_SET to put a new item in the table. + */ + @Test + public void testAppendSetWithKeyAndNonKeyAttributesSpecifiedRecordNotInTable() + throws Exception { + TestItem testItem = new TestItem(); + testItem.setHashKey(UUID.randomUUID().toString()); + testItem.setRangeKey(System.currentTimeMillis()); + testItem.setNonKeyAttribute("new item"); + testItem.setStringSetAttribute(generateRandomStringSet(3)); + + dynamoMapper.save(testItem, appendSetConfig); + + TestItem returnedObject = (TestItem) dynamoMapper.load(testItem); + + assertNotNull(returnedObject); + assertEquals(testItem.getHashKey(), returnedObject.getHashKey()); + assertEquals(testItem.getRangeKey(), returnedObject.getRangeKey()); + assertEquals(testItem.getNonKeyAttribute(), returnedObject.getNonKeyAttribute()); + assertEquals(testItem.getStringSetAttribute(), returnedObject.getStringSetAttribute()); + + } + + /********************************************* + ** CLOBBER ** + *********************************************/ + + /** + * Use CLOBBER to override the existing item by saving a key-only object. + */ + @Test + public void testClobberWithOnlyKeyAttributesSpecifiedRecordInTable() + throws Exception { + /* Put the item with non-key attribute */ + TestItem testItem = putRandomUniqueItem("foo", null); + + /* Override the item by saving a key-only object. */ + testItem.setNonKeyAttribute(null); + dynamoMapper.save(testItem, clobberConfig); + + TestItem returnedObject = (TestItem) dynamoMapper.load(testItem); + + assertNotNull(returnedObject); + assertEquals(testItem.getHashKey(), returnedObject.getHashKey()); + assertEquals(testItem.getRangeKey(), returnedObject.getRangeKey()); + assertNull(returnedObject.getNonKeyAttribute()); + } + + /** + * Use CLOBBER to put a new item with only key attributes. + */ + @Test + public void testClobberWithOnlyKeyAttributesSpecifiedRecordNotInTable() + throws Exception { + TestItem testItem = new TestItem(); + testItem.setHashKey(UUID.randomUUID().toString()); + testItem.setRangeKey(System.currentTimeMillis()); + + dynamoMapper.save(testItem, clobberConfig); + + TestItem returnedObject = (TestItem) dynamoMapper.load(testItem); + + assertNotNull(returnedObject); + assertEquals(testItem.getHashKey(), returnedObject.getHashKey()); + assertEquals(testItem.getRangeKey(), returnedObject.getRangeKey()); + assertNull(returnedObject.getNonKeyAttribute()); + } + + /** + * Use CLOBBER to override the existing item. + */ + @Test + public void testClobberWithKeyAndNonKeyAttributesSpecifiedRecordInTable() + throws Exception { + /* Put the item with non-key attribute */ + TestItem testItem = putRandomUniqueItem("foo", null); + + /* Override the item. */ + testItem.setNonKeyAttribute("not foo"); + dynamoMapper.save(testItem, clobberConfig); + + TestItem returnedObject = (TestItem) dynamoMapper.load(testItem); + + assertNotNull(returnedObject); + assertEquals(testItem.getHashKey(), returnedObject.getHashKey()); + assertEquals(testItem.getRangeKey(), returnedObject.getRangeKey()); + assertEquals(testItem.getNonKeyAttribute(), returnedObject.getNonKeyAttribute()); + } + + /** + * Use CLOBBER to put a new item. + */ + @Test + public void testClobberWithKeyAndNonKeyAttributesSpecifiedRecordNotInTable() + throws Exception { + TestItem testItem = new TestItem(); + testItem.setHashKey(UUID.randomUUID().toString()); + testItem.setRangeKey(System.currentTimeMillis()); + testItem.setNonKeyAttribute("new item"); + + dynamoMapper.save(testItem, clobberConfig); + + TestItem returnedObject = (TestItem) dynamoMapper.load(testItem); + + assertNotNull(returnedObject); + assertEquals(testItem.getHashKey(), returnedObject.getHashKey()); + assertEquals(testItem.getRangeKey(), returnedObject.getRangeKey()); + assertEquals(testItem.getNonKeyAttribute(), returnedObject.getNonKeyAttribute()); + } + + @Test + public void testClobberWithVersion_SucceedsEvenWhenVersionDoesNotMatch() throws Exception { + TestItemWithVersion testItem = putRandomUniqueItemWithVersion(10L); + testItem.setVersion(1L); + testItem.setNonKeyAttribute("new item"); + + dynamoMapper.save(testItem, clobberConfig); + + GetItemResponse item = rawGetItem(testItem); + Map expected = new HashMap(); + expected.put(hashKeyName, AttributeValue.builder().s(testItem.getHashKey()).build()); + expected.put(rangeKeyName, AttributeValue.builder().n(testItem.getRangeKey().toString()).build()); + expected.put(nonKeyAttributeName, AttributeValue.builder().s(testItem.getNonKeyAttribute()).build()); + expected.put(versionAttributeName, AttributeValue.builder().n(Long.toString(2L)).build()); + + assertEquals(expected, item.item()); + } + + /** + * Use CLOBBER to override the existing item by saving a key-only object. + */ + @Test + public void testPutWithOnlyKeyAttributesSpecifiedRecordInTable() + throws Exception { + /* Put the item with non-key attribute */ + TestItem testItem = putRandomUniqueItem("foo", null); + + /* Override the item by saving a key-only object. */ + testItem.setNonKeyAttribute(null); + dynamoMapper.save(testItem, putConfig); + + GetItemResponse item = rawGetItem(testItem); + Map expected = new HashMap(); + expected.put(hashKeyName, AttributeValue.builder().s(testItem.getHashKey()).build()); + expected.put(rangeKeyName, AttributeValue.builder().n(testItem.getRangeKey().toString()).build()); + + assertEquals(expected, item.item()); + } + + @Test + public void testPutWithKeyAndNonKeyAttributesSpecifiedRecordInTable() throws Exception { + /* Put the item with non-key attribute */ + TestItem testItem = putRandomUniqueItem("foo", null); + + /* Override the item. */ + testItem.setNonKeyAttribute("not foo"); + dynamoMapper.save(testItem, putConfig); + + GetItemResponse item = rawGetItem(testItem); + Map expected = new HashMap(); + expected.put(hashKeyName, AttributeValue.builder().s(testItem.getHashKey()).build()); + expected.put(rangeKeyName, AttributeValue.builder().n(testItem.getRangeKey().toString()).build()); + expected.put(nonKeyAttributeName, AttributeValue.builder().s(testItem.getNonKeyAttribute()).build()); + + assertEquals(expected, item.item()); + } + + @Test + public void testPutWithKeyAndNonKeyAttributesSpecifiedRecordNotInTable() + throws Exception { + TestItem testItem = new TestItem(); + testItem.setHashKey(UUID.randomUUID().toString()); + testItem.setRangeKey(System.currentTimeMillis()); + testItem.setNonKeyAttribute("new item"); + + dynamoMapper.save(testItem, putConfig); + + GetItemResponse item = rawGetItem(testItem); + Map expected = new HashMap(); + expected.put(hashKeyName, AttributeValue.builder().s(testItem.getHashKey()).build()); + expected.put(rangeKeyName, AttributeValue.builder().n(testItem.getRangeKey().toString()).build()); + expected.put(nonKeyAttributeName, AttributeValue.builder().s(testItem.getNonKeyAttribute()).build()); + + assertEquals(expected, item.item()); + } + + @Test(expected = ConditionalCheckFailedException.class) + public void testPutWithVersion_FailsWhenVerisonDoesNotMatch() throws Exception { + TestItemWithVersion testItem = putRandomUniqueItemWithVersion(10L); + testItem.setVersion(1L); + testItem.setNonKeyAttribute("new item"); + + dynamoMapper.save(testItem, putConfig); + } + + @Test + public void testPutWithVersion_SucceedsWhenVersionMatches() throws Exception { + Long version = 10L; + TestItemWithVersion testItem = putRandomUniqueItemWithVersion(version); + testItem.setNonKeyAttribute("new item"); + + dynamoMapper.save(testItem, putConfig); + + GetItemResponse item = rawGetItem(testItem); + Map expected = new HashMap(); + expected.put(hashKeyName, AttributeValue.builder().s(testItem.getHashKey()).build()); + expected.put(rangeKeyName, AttributeValue.builder().n(testItem.getRangeKey().toString()).build()); + expected.put(nonKeyAttributeName, AttributeValue.builder().s(testItem.getNonKeyAttribute()).build()); + expected.put(versionAttributeName, AttributeValue.builder().n(Long.toString(version + 1)).build()); + + assertEquals(expected, item.item()); + } + + private GetItemResponse rawGetItem(TestItem testItem) { + Map key = new HashMap(); + key.put(hashKeyName, AttributeValue.builder().s(testItem.getHashKey()).build()); + key.put(rangeKeyName, AttributeValue.builder().n(testItem.getRangeKey().toString()).build()); + return dynamo.getItem( + GetItemRequest.builder() + .tableName(tableName) + .key(key).build()); + } + + private static TestItem putRandomUniqueItem(String nonKeyAttributeValue, Set stringSetAttributeValue) { + String hashKeyValue = UUID.randomUUID().toString(); + Long rangeKeyValue = System.currentTimeMillis(); + Map item = new HashMap(); + item.put(hashKeyName, AttributeValue.builder().s(hashKeyValue).build()); + item.put(rangeKeyName, AttributeValue.builder().n(rangeKeyValue.toString()).build()); + if (null != nonKeyAttributeValue) { + item.put(nonKeyAttributeName, AttributeValue.builder().s(nonKeyAttributeValue).build()); + } + if (null != stringSetAttributeValue) { + item.put(stringSetAttributeName, AttributeValue.builder().ss(stringSetAttributeValue).build()); + } + dynamo.putItem(PutItemRequest.builder().tableName(tableName).item(item).build()); + + /* Returns the item as a modeled object. */ + TestItem testItem = new TestItem(); + testItem.setHashKey(hashKeyValue); + testItem.setRangeKey(rangeKeyValue); + testItem.setNonKeyAttribute(nonKeyAttributeValue); + testItem.setStringSetAttribute(stringSetAttributeValue); + return testItem; + } + + private static TestItemWithVersion putRandomUniqueItemWithVersion(Long version) { + String hashKeyValue = UUID.randomUUID().toString(); + Long rangeKeyValue = System.currentTimeMillis(); + Map item = new HashMap(); + item.put(hashKeyName, AttributeValue.builder().s(hashKeyValue).build()); + item.put(rangeKeyName, AttributeValue.builder().n(rangeKeyValue.toString()).build()); + item.put(versionAttributeName, AttributeValue.builder().n(version.toString()).build()); + dynamo.putItem(PutItemRequest.builder().tableName(tableName).item(item).build()); + TestItemWithVersion testItem = new TestItemWithVersion(); + testItem.setHashKey(hashKeyValue); + testItem.setRangeKey(rangeKeyValue); + testItem.setVersion(version); + return testItem; + } + + private static Set generateRandomStringSet(int size) { + Set result = new HashSet(); + for (int i = 0; i < size; i++) { + result.add(UUID.randomUUID().toString()); + } + return result; + } + + private static boolean assertSetEquals(Set expected, Set actual) { + if (expected == null || actual == null) { + return (expected == null && actual == null); + } + if (expected.size() != actual.size()) { + return false; + } + for (Object item : expected) { + if ( !actual.contains(item) ) { + return false; + } + } + return true; + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/MapperSaveConfigTestBase.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/MapperSaveConfigTestBase.java new file mode 100644 index 000000000000..5879010d0b92 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/MapperSaveConfigTestBase.java @@ -0,0 +1,203 @@ +/* + * 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.mapper.dynamodb.mapper; + +import static software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.ConsistentReads.CONSISTENT; +import static software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.SaveBehavior.APPEND_SET; +import static software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.SaveBehavior.CLOBBER; +import static software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.SaveBehavior.PUT; +import static software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.SaveBehavior.UPDATE; +import static software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.SaveBehavior.UPDATE_SKIP_NULL_ATTRIBUTES; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import software.amazon.awssdk.mapper.dynamodb.test.util.DynamoDBIntegrationTestBase; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAttribute; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBRangeKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBVersionAttribute; +import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition; +import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest; +import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; +import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement; +import software.amazon.awssdk.services.dynamodb.model.KeyType; +import software.amazon.awssdk.services.dynamodb.model.ProvisionedThroughput; +import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType; +import software.amazon.awssdk.services.dynamodb.model.TableDescription; +import software.amazon.awssdk.mapper.dynamodb.pojos.TestItem; +import java.util.Set; +import org.junit.AfterClass; +import org.junit.BeforeClass; + +public class MapperSaveConfigTestBase extends DynamoDBIntegrationTestBase { + + protected static DynamoDBMapper dynamoMapper; + + protected static final DynamoDBMapperConfig defaultConfig = + DynamoDBMapperConfig.builder() + .withSaveBehavior(UPDATE) + .withConsistentReads(CONSISTENT) + .build(); + protected static final DynamoDBMapperConfig updateSkipNullConfig = + DynamoDBMapperConfig.builder() + .withSaveBehavior(UPDATE_SKIP_NULL_ATTRIBUTES) + .withConsistentReads(CONSISTENT) + .build(); + protected static final DynamoDBMapperConfig appendSetConfig = + DynamoDBMapperConfig.builder() + .withSaveBehavior(APPEND_SET) + .withConsistentReads(CONSISTENT) + .build(); + protected static final DynamoDBMapperConfig clobberConfig = + DynamoDBMapperConfig.builder() + .withSaveBehavior(CLOBBER) + .withConsistentReads(CONSISTENT) + .build(); + protected static final DynamoDBMapperConfig putConfig = + DynamoDBMapperConfig.builder() + .withSaveBehavior(PUT) + .withConsistentReads(CONSISTENT) + .build(); + + protected static final String tableName = "aws-java-sdk-dynamodb-mapper-save-config-test"; + + protected static final String hashKeyName = "hashKey"; + + protected static final String rangeKeyName = "rangeKey"; + + protected static final String nonKeyAttributeName = "nonKeyAttribute"; + + protected static final String stringSetAttributeName = "stringSetAttribute"; + + protected static final String versionAttributeName = "version"; + + /** Read capacity for the test table being created in Amazon DynamoDB. */ + protected static final Long READ_CAPACITY = 10L; + + /** Write capacity for the test table being created in Amazon DynamoDB. */ + protected static final Long WRITE_CAPACITY = 5L; + + /** Provisioned Throughput for the test table created in Amazon DynamoDB */ + protected static final ProvisionedThroughput DEFAULT_PROVISIONED_THROUGHPUT = ProvisionedThroughput.builder() + .readCapacityUnits(READ_CAPACITY).writeCapacityUnits( + WRITE_CAPACITY).build(); + + @BeforeClass + public static void setUp() throws Exception { + setUpTestBase(); + dynamoMapper = new DynamoDBMapper(dynamo); + + createTestTable(DEFAULT_PROVISIONED_THROUGHPUT); + dynamo.waiter().waitUntilTableExists(b -> b.tableName(tableName)); + } + + @AfterClass + public static void tearDown() { + dynamo.deleteTable(DeleteTableRequest.builder().tableName(tableName).build()); + } + + @DynamoDBTable(tableName = tableName) + public static class TestItemWithVersion extends TestItem { + + @DynamoDBVersionAttribute + private Long version; + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + } + + @DynamoDBTable(tableName = tableName) + static public class TestAppendToScalarItem { + + private String hashKey; + private Long rangeKey; + private Set fakeStringSetAttribute; + + @DynamoDBHashKey(attributeName = hashKeyName) + public String getHashKey() { + return hashKey; + } + + public void setHashKey(String hashKey) { + this.hashKey = hashKey; + } + + @DynamoDBRangeKey(attributeName = rangeKeyName) + public Long getRangeKey() { + return rangeKey; + } + + public void setRangeKey(Long rangeKey) { + this.rangeKey = rangeKey; + } + + @DynamoDBAttribute(attributeName = nonKeyAttributeName) + public Set getFakeStringSetAttribute() { + return fakeStringSetAttribute; + } + + public void setFakeStringSetAttribute(Set stringSetAttribute) { + this.fakeStringSetAttribute = stringSetAttribute; + } + } + + /** + * Helper method to create a table in Amazon DynamoDB + */ + protected static void createTestTable( + ProvisionedThroughput provisionedThroughput) { + CreateTableRequest createTableRequest = CreateTableRequest.builder() + .tableName(tableName) + .keySchema( + KeySchemaElement.builder().attributeName( + hashKeyName).keyType( + KeyType.HASH).build(), + KeySchemaElement.builder().attributeName( + rangeKeyName).keyType( + KeyType.RANGE).build()) + .attributeDefinitions( + AttributeDefinition.builder().attributeName( + hashKeyName).attributeType( + ScalarAttributeType.S).build(), + AttributeDefinition.builder().attributeName( + rangeKeyName).attributeType( + ScalarAttributeType.N).build()) + .provisionedThroughput(provisionedThroughput) + .build(); + + TableDescription createdTableDescription = dynamo.createTable( + createTableRequest).tableDescription(); + System.out.println("Created Table: " + createdTableDescription); + assertEquals(tableName, createdTableDescription.tableName()); + assertNotNull(createdTableDescription.tableStatus()); + assertEquals(hashKeyName, createdTableDescription + .keySchema().get(0).attributeName()); + assertEquals(KeyType.HASH.toString(), createdTableDescription + .keySchema().get(0).keyTypeAsString()); + assertEquals(rangeKeyName, createdTableDescription + .keySchema().get(1).attributeName()); + assertEquals(KeyType.RANGE.toString(), createdTableDescription + .keySchema().get(1).keyTypeAsString()); + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/NoSuchTableClass.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/NoSuchTableClass.java new file mode 100644 index 000000000000..26f7d1a38cf5 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/NoSuchTableClass.java @@ -0,0 +1,43 @@ +/* + * 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.mapper.dynamodb.mapper; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; + +@DynamoDBTable(tableName = "tableNotExist") +public class NoSuchTableClass { + + private String key; + private String otherAttribute; + + @DynamoDBHashKey + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getOtherAttribute() { + return otherAttribute; + } + + public void setOtherAttribute(String otherAttribute) { + this.otherAttribute = otherAttribute; + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/NumberAttributeClass.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/NumberAttributeClass.java new file mode 100644 index 000000000000..eaf809f84217 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/NumberAttributeClass.java @@ -0,0 +1,340 @@ +/* + * 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.mapper.dynamodb.mapper; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Calendar; +import java.util.Date; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAttribute; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAutoGeneratedKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBIgnore; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; + +/** + * Simple domain class with numeric attributes + */ +@DynamoDBTable(tableName = "aws-java-sdk-util") +public class NumberAttributeClass { + + private String key; + private int intAttribute; + private Integer integerAttribute; + private double doubleAttribute; + private Double doubleObjectAttribute; + private float floatAttribute; + private Float floatObjectAttribute; + private BigDecimal bigDecimalAttribute; + private BigInteger bigIntegerAttribute; + private long longAttribute; + private Long longObjectAttribute; + private short shortAttribute; + private Short shortObjectAttribute; + private byte byteAttribute; + private Byte byteObjectAttribute; + private Date dateAttribute; + private Calendar calendarAttribute; + private Boolean booleanObjectAttribute; + private boolean booleanAttribute; + private String ignored = "notSent"; + + @DynamoDBAutoGeneratedKey + @DynamoDBHashKey + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public int getIntAttribute() { + return intAttribute; + } + + public void setIntAttribute(int intAttribute) { + this.intAttribute = intAttribute; + } + + public Integer getIntegerAttribute() { + return integerAttribute; + } + + public void setIntegerAttribute(Integer integerAttribute) { + this.integerAttribute = integerAttribute; + } + + public double getDoubleAttribute() { + return doubleAttribute; + } + + public void setDoubleAttribute(double doubleAttribute) { + this.doubleAttribute = doubleAttribute; + } + + public Double getDoubleObjectAttribute() { + return doubleObjectAttribute; + } + + public void setDoubleObjectAttribute(Double doubleObjectAttribute) { + this.doubleObjectAttribute = doubleObjectAttribute; + } + + @DynamoDBAttribute + public float getFloatAttribute() { + return floatAttribute; + } + + public void setFloatAttribute(float floatAttribute) { + this.floatAttribute = floatAttribute; + } + + public Float getFloatObjectAttribute() { + return floatObjectAttribute; + } + + public void setFloatObjectAttribute(Float floatObjectAttribute) { + this.floatObjectAttribute = floatObjectAttribute; + } + + public BigDecimal getBigDecimalAttribute() { + return bigDecimalAttribute; + } + + public void setBigDecimalAttribute(BigDecimal bigDecimalAttribute) { + this.bigDecimalAttribute = bigDecimalAttribute; + } + + public BigInteger getBigIntegerAttribute() { + return bigIntegerAttribute; + } + + public void setBigIntegerAttribute(BigInteger bigIntegerAttribute) { + this.bigIntegerAttribute = bigIntegerAttribute; + } + + public long getLongAttribute() { + return longAttribute; + } + + public void setLongAttribute(long longAttribute) { + this.longAttribute = longAttribute; + } + + public Long getLongObjectAttribute() { + return longObjectAttribute; + } + + public void setLongObjectAttribute(Long longObjectAttribute) { + this.longObjectAttribute = longObjectAttribute; + } + + public byte getByteAttribute() { + return byteAttribute; + } + + public void setByteAttribute(byte byteAttribute) { + this.byteAttribute = byteAttribute; + } + + public Byte getByteObjectAttribute() { + return byteObjectAttribute; + } + + public void setByteObjectAttribute(Byte byteObjectAttribute) { + this.byteObjectAttribute = byteObjectAttribute; + } + + public Date getDateAttribute() { + return dateAttribute; + } + + public void setDateAttribute(Date dateAttribute) { + this.dateAttribute = dateAttribute; + } + + public Calendar getCalendarAttribute() { + return calendarAttribute; + } + + public void setCalendarAttribute(Calendar calendarAttribute) { + this.calendarAttribute = calendarAttribute; + } + + public Boolean getBooleanObjectAttribute() { + return booleanObjectAttribute; + } + + public void setBooleanObjectAttribute(Boolean booleanObjectAttribute) { + this.booleanObjectAttribute = booleanObjectAttribute; + } + + public boolean isBooleanAttribute() { + return booleanAttribute; + } + + public void setBooleanAttribute(boolean booleanAttribute) { + this.booleanAttribute = booleanAttribute; + } + + @DynamoDBIgnore + public String getIgnored() { + return ignored; + } + + public void setIgnored(String ignored) { + this.ignored = ignored; + } + + public short getShortAttribute() { + return shortAttribute; + } + + public void setShortAttribute(short shortAttribute) { + this.shortAttribute = shortAttribute; + } + + public Short getShortObjectAttribute() { + return shortObjectAttribute; + } + + public void setShortObjectAttribute(Short shortObjectAttribute) { + this.shortObjectAttribute = shortObjectAttribute; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((bigDecimalAttribute == null) ? 0 : bigDecimalAttribute.hashCode()); + result = prime * result + ((bigIntegerAttribute == null) ? 0 : bigIntegerAttribute.hashCode()); + result = prime * result + (booleanAttribute ? 1231 : 1237); + result = prime * result + ((booleanObjectAttribute == null) ? 0 : booleanObjectAttribute.hashCode()); + result = prime * result + byteAttribute; + result = prime * result + ((byteObjectAttribute == null) ? 0 : byteObjectAttribute.hashCode()); + result = prime * result + ((calendarAttribute == null) ? 0 : calendarAttribute.hashCode()); + result = prime * result + ((dateAttribute == null) ? 0 : dateAttribute.hashCode()); + long temp; + temp = Double.doubleToLongBits(doubleAttribute); + result = prime * result + (int) (temp ^ (temp >>> 32)); + result = prime * result + ((doubleObjectAttribute == null) ? 0 : doubleObjectAttribute.hashCode()); + result = prime * result + Float.floatToIntBits(floatAttribute); + result = prime * result + ((floatObjectAttribute == null) ? 0 : floatObjectAttribute.hashCode()); + result = prime * result + ((ignored == null) ? 0 : ignored.hashCode()); + result = prime * result + intAttribute; + result = prime * result + ((integerAttribute == null) ? 0 : integerAttribute.hashCode()); + result = prime * result + ((key == null) ? 0 : key.hashCode()); + result = prime * result + (int) (longAttribute ^ (longAttribute >>> 32)); + result = prime * result + ((longObjectAttribute == null) ? 0 : longObjectAttribute.hashCode()); + result = prime * result + shortAttribute; + result = prime * result + ((shortObjectAttribute == null) ? 0 : shortObjectAttribute.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) + return true; + if ( obj == null ) + return false; + if ( getClass() != obj.getClass() ) + return false; + NumberAttributeClass other = (NumberAttributeClass) obj; + if ( bigDecimalAttribute == null ) { + if ( other.bigDecimalAttribute != null ) + return false; + } else if ( !bigDecimalAttribute.equals(other.bigDecimalAttribute) ) + return false; + if ( bigIntegerAttribute == null ) { + if ( other.bigIntegerAttribute != null ) + return false; + } else if ( !bigIntegerAttribute.equals(other.bigIntegerAttribute) ) + return false; + if ( booleanAttribute != other.booleanAttribute ) + return false; + if ( booleanObjectAttribute == null ) { + if ( other.booleanObjectAttribute != null ) + return false; + } else if ( !booleanObjectAttribute.equals(other.booleanObjectAttribute) ) + return false; + if ( byteAttribute != other.byteAttribute ) + return false; + if ( byteObjectAttribute == null ) { + if ( other.byteObjectAttribute != null ) + return false; + } else if ( !byteObjectAttribute.equals(other.byteObjectAttribute) ) + return false; + if ( calendarAttribute == null ) { + if ( other.calendarAttribute != null ) + return false; + } else if ( !calendarAttribute.equals(other.calendarAttribute) ) + return false; + if ( dateAttribute == null ) { + if ( other.dateAttribute != null ) + return false; + } else if ( !dateAttribute.equals(other.dateAttribute) ) + return false; + if ( Double.doubleToLongBits(doubleAttribute) != Double.doubleToLongBits(other.doubleAttribute) ) + return false; + if ( doubleObjectAttribute == null ) { + if ( other.doubleObjectAttribute != null ) + return false; + } else if ( !doubleObjectAttribute.equals(other.doubleObjectAttribute) ) + return false; + if ( Float.floatToIntBits(floatAttribute) != Float.floatToIntBits(other.floatAttribute) ) + return false; + if ( floatObjectAttribute == null ) { + if ( other.floatObjectAttribute != null ) + return false; + } else if ( !floatObjectAttribute.equals(other.floatObjectAttribute) ) + return false; + if ( ignored == null ) { + if ( other.ignored != null ) + return false; + } else if ( !ignored.equals(other.ignored) ) + return false; + if ( intAttribute != other.intAttribute ) + return false; + if ( integerAttribute == null ) { + if ( other.integerAttribute != null ) + return false; + } else if ( !integerAttribute.equals(other.integerAttribute) ) + return false; + if ( key == null ) { + if ( other.key != null ) + return false; + } else if ( !key.equals(other.key) ) + return false; + if ( longAttribute != other.longAttribute ) + return false; + if ( longObjectAttribute == null ) { + if ( other.longObjectAttribute != null ) + return false; + } else if ( !longObjectAttribute.equals(other.longObjectAttribute) ) + return false; + if ( shortAttribute != other.shortAttribute ) + return false; + if ( shortObjectAttribute == null ) { + if ( other.shortObjectAttribute != null ) + return false; + } else if ( !shortObjectAttribute.equals(other.shortObjectAttribute) ) + return false; + return true; + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/NumberSetAttributeClass.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/NumberSetAttributeClass.java new file mode 100644 index 000000000000..593434906d75 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/NumberSetAttributeClass.java @@ -0,0 +1,243 @@ +/* + * 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.mapper.dynamodb.mapper; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Calendar; +import java.util.Date; +import java.util.Set; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAttribute; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; + +/** + * Simple domain class with numeric attributes + */ +@DynamoDBTable(tableName = "aws-java-sdk-util") +public class NumberSetAttributeClass { + + private String key; + private Set integerAttribute; + private Set doubleObjectAttribute; + private Set floatObjectAttribute; + private Set bigDecimalAttribute; + private Set bigIntegerAttribute; + private Set longObjectAttribute; + private Set byteObjectAttribute; + private Set dateAttribute; + private Set calendarAttribute; + private Set booleanAttribute; + + @DynamoDBHashKey + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + @DynamoDBAttribute + public Set getIntegerAttribute() { + return integerAttribute; + } + + public void setIntegerAttribute(Set integerAttribute) { + this.integerAttribute = integerAttribute; + } + + @DynamoDBAttribute + public Set getDoubleObjectAttribute() { + return doubleObjectAttribute; + } + + public void setDoubleObjectAttribute(Set doubleObjectAttribute) { + this.doubleObjectAttribute = doubleObjectAttribute; + } + + @DynamoDBAttribute + public Set getFloatObjectAttribute() { + return floatObjectAttribute; + } + + public void setFloatObjectAttribute(Set floatObjectAttribute) { + this.floatObjectAttribute = floatObjectAttribute; + } + + @DynamoDBAttribute + public Set getBigDecimalAttribute() { + return bigDecimalAttribute; + } + + public void setBigDecimalAttribute(Set bigDecimalAttribute) { + this.bigDecimalAttribute = bigDecimalAttribute; + } + + @DynamoDBAttribute + public Set getBigIntegerAttribute() { + return bigIntegerAttribute; + } + + public void setBigIntegerAttribute(Set bigIntegerAttribute) { + this.bigIntegerAttribute = bigIntegerAttribute; + } + + @DynamoDBAttribute + public Set getLongObjectAttribute() { + return longObjectAttribute; + } + + public void setLongObjectAttribute(Set longObjectAttribute) { + this.longObjectAttribute = longObjectAttribute; + } + + @DynamoDBAttribute + public Set getByteObjectAttribute() { + return byteObjectAttribute; + } + + public void setByteObjectAttribute(Set byteObjectAttribute) { + this.byteObjectAttribute = byteObjectAttribute; + } + + @DynamoDBAttribute + public Set getDateAttribute() { + return dateAttribute; + } + + public void setDateAttribute(Set dateAttribute) { + this.dateAttribute = dateAttribute; + } + + @DynamoDBAttribute + public Set getCalendarAttribute() { + return calendarAttribute; + } + + public void setCalendarAttribute(Set calendarAttribute) { + this.calendarAttribute = calendarAttribute; + } + + @DynamoDBAttribute + public Set getBooleanAttribute() { + return booleanAttribute; + } + + public void setBooleanAttribute(Set booleanAttribute) { + this.booleanAttribute = booleanAttribute; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((bigDecimalAttribute == null) ? 0 : bigDecimalAttribute.hashCode()); + result = prime * result + ((bigIntegerAttribute == null) ? 0 : bigIntegerAttribute.hashCode()); + result = prime * result + ((booleanAttribute == null) ? 0 : booleanAttribute.hashCode()); + result = prime * result + ((byteObjectAttribute == null) ? 0 : byteObjectAttribute.hashCode()); + result = prime * result + ((calendarAttribute == null) ? 0 : calendarAttribute.hashCode()); + result = prime * result + ((dateAttribute == null) ? 0 : dateAttribute.hashCode()); + result = prime * result + ((doubleObjectAttribute == null) ? 0 : doubleObjectAttribute.hashCode()); + result = prime * result + ((floatObjectAttribute == null) ? 0 : floatObjectAttribute.hashCode()); + result = prime * result + ((integerAttribute == null) ? 0 : integerAttribute.hashCode()); + result = prime * result + ((key == null) ? 0 : key.hashCode()); + result = prime * result + ((longObjectAttribute == null) ? 0 : longObjectAttribute.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) + return true; + if ( obj == null ) + return false; + if ( getClass() != obj.getClass() ) + return false; + NumberSetAttributeClass other = (NumberSetAttributeClass) obj; + if ( bigDecimalAttribute == null ) { + if ( other.bigDecimalAttribute != null ) + return false; + } else if ( !bigDecimalAttribute.equals(other.bigDecimalAttribute) ) + return false; + if ( bigIntegerAttribute == null ) { + if ( other.bigIntegerAttribute != null ) + return false; + } else if ( !bigIntegerAttribute.equals(other.bigIntegerAttribute) ) + return false; + if ( booleanAttribute == null ) { + if ( other.booleanAttribute != null ) + return false; + } else if ( !booleanAttribute.equals(other.booleanAttribute) ) + return false; + if ( byteObjectAttribute == null ) { + if ( other.byteObjectAttribute != null ) + return false; + } else if ( !byteObjectAttribute.equals(other.byteObjectAttribute) ) + return false; + if ( calendarAttribute == null ) { + if ( other.calendarAttribute != null ) + return false; + } else if ( !calendarAttribute.equals(other.calendarAttribute) ) + return false; + if ( dateAttribute == null ) { + if ( other.dateAttribute != null ) + return false; + } else if ( !dateAttribute.equals(other.dateAttribute) ) + return false; + if ( doubleObjectAttribute == null ) { + if ( other.doubleObjectAttribute != null ) + return false; + } else if ( !doubleObjectAttribute.equals(other.doubleObjectAttribute) ) + return false; + if ( floatObjectAttribute == null ) { + if ( other.floatObjectAttribute != null ) + return false; + } else if ( !floatObjectAttribute.equals(other.floatObjectAttribute) ) + return false; + if ( integerAttribute == null ) { + if ( other.integerAttribute != null ) + return false; + } else if ( !integerAttribute.equals(other.integerAttribute) ) + return false; + if ( key == null ) { + if ( other.key != null ) + return false; + } else if ( !key.equals(other.key) ) + return false; + if ( longObjectAttribute == null ) { + if ( other.longObjectAttribute != null ) + return false; + } else if ( !longObjectAttribute.equals(other.longObjectAttribute) ) + return false; + return true; + } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "NumberSetAttributeClass [key=" + key; +// + ", integerAttribute=" + integerAttribute +// + ", doubleObjectAttribute=" + doubleObjectAttribute + ", floatObjectAttribute=" + floatObjectAttribute +// + ", bigDecimalAttribute=" + bigDecimalAttribute + ", bigIntegerAttribute=" + bigIntegerAttribute +// + ", longObjectAttribute=" + longObjectAttribute + ", byteObjectAttribute=" + byteObjectAttribute +// + ", dateAttribute=" + dateAttribute + ", calendarAttribute=" + calendarAttribute +// + ", booleanAttribute=" + booleanAttribute + "]"; + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/NumericSetAttributesIntegrationTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/NumericSetAttributesIntegrationTest.java new file mode 100644 index 000000000000..3da2436f988d --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/NumericSetAttributesIntegrationTest.java @@ -0,0 +1,170 @@ +/* + * 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.mapper.dynamodb.mapper; + +import static org.junit.Assert.*; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.junit.BeforeClass; +import org.junit.Test; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperIntegrationTestBase; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; + +/** + * Tests string set attributes + */ +public class NumericSetAttributesIntegrationTest extends DynamoDBMapperIntegrationTestBase { + + private static final String INTEGER_ATTRIBUTE = "integerAttribute"; + private static final String FLOAT_OBJECT_ATTRIBUTE = "floatObjectAttribute"; + private static final String DOUBLE_OBJECT_ATTRIBUTE = "doubleObjectAttribute"; + private static final String BIG_INTEGER_ATTRIBUTE = "bigIntegerAttribute"; + private static final String BIG_DECIMAL_ATTRIBUTE = "bigDecimalAttribute"; + private static final String LONG_OBJECT_ATTRIBUTE = "longObjectAttribute"; + private static final String BYTE_OBJECT_ATTRIBUTE = "byteObjectAttribute"; + private static final String BOOLEAN_ATTRIBUTE = "booleanAttribute"; + + // We don't start with the current system millis like other tests because + // it's out of the range of some data types + private static int start = 1; + private static int byteStart = 1; + + private static final List> attrs = new LinkedList>(); + + // Test data + static { + for ( int i = 0; i < 5; i++ ) { + Map attr = new HashMap(); + attr.put(KEY_NAME, AttributeValue.builder().s("" + start++).build()); + attr.put(INTEGER_ATTRIBUTE, AttributeValue.builder().ns("" + start++, "" + start++, "" + start++).build()); + attr.put(FLOAT_OBJECT_ATTRIBUTE, AttributeValue.builder().ns("" + start++, "" + start++, "" + start++).build()); + attr.put(DOUBLE_OBJECT_ATTRIBUTE, AttributeValue.builder().ns("" + start++, "" + start++, "" + start++).build()); + attr.put(BIG_INTEGER_ATTRIBUTE, AttributeValue.builder().ns("" + start++, "" + start++, "" + start++).build()); + attr.put(BIG_DECIMAL_ATTRIBUTE, AttributeValue.builder().ns("" + start++, "" + start++, "" + start++).build()); + attr.put(LONG_OBJECT_ATTRIBUTE, AttributeValue.builder().ns("" + start++, "" + start++, "" + start++).build()); + attr.put(BYTE_OBJECT_ATTRIBUTE, AttributeValue.builder().ns("" + byteStart++, "" + byteStart++, "" + byteStart++).build()); + attr.put(BOOLEAN_ATTRIBUTE, AttributeValue.builder().ns("0", "1").build()); + attrs.add(attr); + } + }; + + @BeforeClass + public static void setUp() throws Exception { + DynamoDBMapperIntegrationTestBase.setUp(); + + // Insert the data + for ( Map attr : attrs ) { + dynamo.putItem(PutItemRequest.builder().tableName(TABLE_NAME).item(attr).build()); + } + } + + @Test + public void testLoad() throws Exception { + DynamoDBMapper util = new DynamoDBMapper(dynamo); + + for ( Map attr : attrs ) { + NumberSetAttributeClass x = util.load(NumberSetAttributeClass.class, attr.get(KEY_NAME).s()); + assertEquals(x.getKey(), attr.get(KEY_NAME).s()); + + // Convert all numbers to the most inclusive type for easy comparison + assertNumericSetsEquals(x.getBigDecimalAttribute(), attr.get(BIG_DECIMAL_ATTRIBUTE).ns()); + assertNumericSetsEquals(x.getBigIntegerAttribute(), attr.get(BIG_INTEGER_ATTRIBUTE).ns()); + assertNumericSetsEquals(x.getFloatObjectAttribute(), attr.get(FLOAT_OBJECT_ATTRIBUTE).ns()); + assertNumericSetsEquals(x.getDoubleObjectAttribute(), attr.get(DOUBLE_OBJECT_ATTRIBUTE).ns()); + assertNumericSetsEquals(x.getIntegerAttribute(), attr.get(INTEGER_ATTRIBUTE).ns()); + assertNumericSetsEquals(x.getLongObjectAttribute(), attr.get(LONG_OBJECT_ATTRIBUTE).ns()); + assertNumericSetsEquals(x.getByteObjectAttribute(), attr.get(BYTE_OBJECT_ATTRIBUTE).ns()); + assertSetsEqual(toSet("0", "1"), attr.get(BOOLEAN_ATTRIBUTE).ns()); + } + } + + @Test + public void testSave() throws Exception { + List objs = new ArrayList(); + for ( int i = 0; i < 5; i++ ) { + NumberSetAttributeClass obj = getUniqueObject(); + objs.add(obj); + } + + DynamoDBMapper util = new DynamoDBMapper(dynamo); + for (NumberSetAttributeClass obj : objs) { + util.save(obj); + } + + for (NumberSetAttributeClass obj : objs) { + NumberSetAttributeClass loaded = util.load(NumberSetAttributeClass.class, obj.getKey()); + assertEquals(obj, loaded); + } + } + + @Test + public void testUpdate() throws Exception { + List objs = new ArrayList(); + for ( int i = 0; i < 5; i++ ) { + NumberSetAttributeClass obj = getUniqueObject(); + objs.add(obj); + } + + DynamoDBMapper util = new DynamoDBMapper(dynamo); + for (NumberSetAttributeClass obj : objs) { + util.save(obj); + } + + for ( NumberSetAttributeClass obj : objs ) { + NumberSetAttributeClass replacement = getUniqueObject(); + replacement.setKey(obj.getKey()); + util.save(replacement); + assertEquals(replacement, util.load(NumberSetAttributeClass.class, obj.getKey())); + } + } + + private NumberSetAttributeClass getUniqueObject() { + NumberSetAttributeClass obj = new NumberSetAttributeClass(); + obj.setKey(String.valueOf(startKey++)); + obj.setBigDecimalAttribute(toSet(new BigDecimal(startKey++), new BigDecimal(startKey++), new BigDecimal(startKey++))); + obj.setBigIntegerAttribute(toSet(new BigInteger("" + startKey++), new BigInteger("" + startKey++), new BigInteger("" + startKey++))); + obj.setByteObjectAttribute(toSet(new Byte("" + byteStart++), new Byte("" + byteStart++), new Byte("" + byteStart++))); + obj.setDoubleObjectAttribute(toSet(new Double("" + start++), new Double("" + start++), new Double("" + start++))); + obj.setFloatObjectAttribute(toSet(new Float("" + start++), new Float("" + start++), new Float("" + start++))); + obj.setIntegerAttribute(toSet(new Integer("" + start++), new Integer("" + start++), new Integer("" + start++))); + obj.setLongObjectAttribute(toSet(new Long("" + start++), new Long("" + start++), new Long("" + start++))); + obj.setBooleanAttribute(toSet(true, false)); + obj.setDateAttribute(toSet(new Date(startKey++), new Date(startKey++), new Date(startKey++))); + Set cals = new HashSet(); + for ( Date d : obj.getDateAttribute() ) { + Calendar cal = GregorianCalendar.getInstance(); + cal.setTime(d); + cals.add(cal); + } + obj.setCalendarAttribute(toSet(cals)); + return obj; + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/QueryIntegrationTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/QueryIntegrationTest.java new file mode 100644 index 000000000000..c820fe75d131 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/QueryIntegrationTest.java @@ -0,0 +1,194 @@ +/* + * 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.mapper.dynamodb.mapper; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperIntegrationTestBase; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.ConsistentReads; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBQueryExpression; +import software.amazon.awssdk.mapper.dynamodb.QueryResultPage; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.ComparisonOperator; +import software.amazon.awssdk.services.dynamodb.model.Condition; +import software.amazon.awssdk.services.dynamodb.model.Select; +import software.amazon.awssdk.mapper.dynamodb.pojos.RangeKeyClass; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Random; +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * Integration tests for the query operation on DynamoDBMapper. + */ +public class QueryIntegrationTest extends DynamoDBMapperIntegrationTestBase { + + private static final long HASH_KEY = System.currentTimeMillis(); + private static RangeKeyClass hashKeyObject; + private static final int TEST_ITEM_NUMBER = 500; + private static DynamoDBMapper mapper; + + @BeforeClass + public static void setUp() throws Exception { + setUpTableWithRangeAttribute(); + + DynamoDBMapperConfig mapperConfig = new DynamoDBMapperConfig(ConsistentReads.CONSISTENT); + mapper = new DynamoDBMapper(dynamo, mapperConfig); + + putTestData(mapper, TEST_ITEM_NUMBER); + + hashKeyObject = new RangeKeyClass(); + hashKeyObject.setKey(HASH_KEY); + } + + @Test + public void testQueryWithSelectCount() throws Exception { + DynamoDBQueryExpression countExpression = + new DynamoDBQueryExpression().withHashKeyValues(hashKeyObject) + .withSelect(Select.COUNT); + QueryResultPage list = mapper.queryPage(RangeKeyClass.class, countExpression); + assertEquals((Integer) TEST_ITEM_NUMBER, list.getCount()); + } + + @Test + public void testQueryWithPrimaryRangeKey() throws Exception { + DynamoDBQueryExpression queryExpression = + new DynamoDBQueryExpression() + .withHashKeyValues(hashKeyObject) + .withRangeKeyCondition( + "rangeKey", + Condition.builder() + .comparisonOperator(ComparisonOperator.GT) + .attributeValueList(AttributeValue.builder().n("1.0").build()).build()) + .withLimit(11); + List list = mapper.query(RangeKeyClass.class, queryExpression); + + int count = 0; + Iterator iterator = list.iterator(); + while ( iterator.hasNext() ) { + count++; + RangeKeyClass next = iterator.next(); + assertTrue(next.getRangeKey() > 1.00); + } + + int numMatchingObjects = TEST_ITEM_NUMBER - 2; + assertEquals(count, numMatchingObjects); + assertEquals(numMatchingObjects, list.size()); + + assertNotNull(list.get(list.size() / 2)); + assertTrue(list.contains(list.get(list.size() / 2))); + assertEquals(numMatchingObjects, list.toArray().length); + + Thread.sleep(250); + int totalCount = mapper.count(RangeKeyClass.class, queryExpression); + assertEquals(numMatchingObjects, totalCount); + + /** + * Tests query with only hash key + */ + queryExpression = new DynamoDBQueryExpression().withHashKeyValues(hashKeyObject); + list = mapper.query(RangeKeyClass.class, queryExpression); + assertEquals(TEST_ITEM_NUMBER, list.size()); + } + + /** + * Tests making queries using query filter on non-key attributes. + */ + @Test + public void testQueryFilter() { + // A random filter condition to be applied to the query. + Random random = new Random(); + int randomFilterValue = random.nextInt(TEST_ITEM_NUMBER); + Condition filterCondition = Condition.builder() + .comparisonOperator(ComparisonOperator.LT) + .attributeValueList( + AttributeValue.builder().n(Integer.toString(randomFilterValue)).build()).build(); + + /* + * (1) Apply the filter on the range key, in form of key condition + */ + DynamoDBQueryExpression queryWithRangeKeyCondition = + new DynamoDBQueryExpression() + .withHashKeyValues(hashKeyObject) + .withRangeKeyCondition("rangeKey", filterCondition); + List rangeKeyConditionResult = mapper.query(RangeKeyClass.class, queryWithRangeKeyCondition); + + /* + * (2) Apply the filter on the bigDecimalAttribute, in form of query filter + */ + DynamoDBQueryExpression queryWithQueryFilterCondition = + new DynamoDBQueryExpression() + .withHashKeyValues(hashKeyObject) + .withQueryFilter(Collections.singletonMap("bigDecimalAttribute", filterCondition)); + List queryFilterResult = mapper.query(RangeKeyClass.class, queryWithQueryFilterCondition); + + assertEquals(rangeKeyConditionResult.size(), queryFilterResult.size()); + for (int i = 0; i < rangeKeyConditionResult.size(); i++) { + assertEquals(rangeKeyConditionResult.get(i), queryFilterResult.get(i)); + } + } + + /** + * Tests that exception should be raised when user provides an index name + * when making query with the primary range key. + */ + @Test + public void testUnnecessaryIndexNameException() { + try{ + DynamoDBMapper mapper = new DynamoDBMapper(dynamo); + long hashKey = System.currentTimeMillis(); + RangeKeyClass keyObject = new RangeKeyClass(); + keyObject.setKey(hashKey); + DynamoDBQueryExpression queryExpression = new DynamoDBQueryExpression().withHashKeyValues(keyObject); + queryExpression.withRangeKeyCondition("rangeKey", + Condition.builder().comparisonOperator(ComparisonOperator.GT.toString()).attributeValueList( + AttributeValue.builder().n("1.0").build()).build()).withLimit(11) + .withIndexName("some_index"); + mapper.query(RangeKeyClass.class, queryExpression); + fail("User should not provide index name when making query with the primary range key"); + } catch (IllegalArgumentException expected) { + System.out.println(expected.getMessage()); + } catch (Exception e) { + fail("Should trigger AmazonClientException."); + } + + } + + /** + * Use BatchSave to put some test data into the tested table. Each item is + * hash-keyed by the same value, and range-keyed by numbers starting from 0. + */ + private static void putTestData(DynamoDBMapper mapper, int itemNumber) { + List objs = new ArrayList(); + for ( int i = 0; i < itemNumber; i++ ) { + RangeKeyClass obj = new RangeKeyClass(); + obj.setKey(HASH_KEY); + obj.setRangeKey(i); + obj.setBigDecimalAttribute(new BigDecimal(i)); + objs.add(obj); + } + mapper.batchSave(objs); + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/RangeKeyAttributesIntegrationTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/RangeKeyAttributesIntegrationTest.java new file mode 100644 index 000000000000..21a5442c4eea --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/RangeKeyAttributesIntegrationTest.java @@ -0,0 +1,170 @@ +/* + * 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.mapper.dynamodb.mapper; + +import static org.junit.Assert.*; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.junit.BeforeClass; +import org.junit.Test; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperIntegrationTestBase; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; +import software.amazon.awssdk.mapper.dynamodb.pojos.RangeKeyClass; + +/** + * Tests range and hash key combination + */ +public class RangeKeyAttributesIntegrationTest extends DynamoDBMapperIntegrationTestBase { + + private static final String RANGE_KEY = "rangeKey"; + private static final String INTEGER_ATTRIBUTE = "integerSetAttribute"; + private static final String BIG_DECIMAL_ATTRIBUTE = "bigDecimalAttribute"; + private static final String STRING_SET_ATTRIBUTE = "stringSetAttribute"; + private static final String STRING_ATTRIBUTE = "stringAttribute"; + private static final String VERSION_ATTRIBUTE = "version"; + + + // We don't start with the current system millis like other tests because + // it's out of the range of some data types + private static int start = 1; + + private static final List> attrs = new LinkedList>(); + + // Test data + static { + for ( int i = 0; i < 5; i++ ) { + Map attr = new HashMap(); + attr.put(KEY_NAME, AttributeValue.builder().n("" + startKey++).build()); + attr.put(RANGE_KEY, AttributeValue.builder().n("" + start++).build()); + attr.put(INTEGER_ATTRIBUTE, AttributeValue.builder().ns("" + start++, "" + start++, "" + start++).build()); + attr.put(BIG_DECIMAL_ATTRIBUTE, AttributeValue.builder().n("" + start++).build()); + attr.put(STRING_ATTRIBUTE, AttributeValue.builder().s("" + start++).build()); + attr.put(STRING_SET_ATTRIBUTE, AttributeValue.builder().ss("" + start++, "" + start++, "" + start++).build()); + attr.put(VERSION_ATTRIBUTE, AttributeValue.builder().n("1").build()); + + attrs.add(attr); + } + }; + + @BeforeClass + public static void setUp() throws Exception { + setUpTableWithRangeAttribute(); + + // Insert the data + for ( Map attr : attrs ) { + dynamo.putItem(PutItemRequest.builder().tableName(TABLE_WITH_RANGE_ATTRIBUTE).item(attr).build()); + } + } + + @Test + public void testLoad() throws Exception { + DynamoDBMapper util = new DynamoDBMapper(dynamo); + + for ( Map attr : attrs ) { + RangeKeyClass x = util.load(newRangeKey(Long.parseLong(attr.get(KEY_NAME).n()), + Double.parseDouble(attr.get(RANGE_KEY).n()))); + + // Convert all numbers to the most inclusive type for easy + // comparison + assertEquals(new BigDecimal(x.getKey()), new BigDecimal(attr.get(KEY_NAME).n())); + assertEquals(new BigDecimal(x.getRangeKey()), new BigDecimal(attr.get(RANGE_KEY).n())); + assertEquals(new BigDecimal(x.getVersion()), new BigDecimal(attr.get(VERSION_ATTRIBUTE).n())); + assertEquals(x.getBigDecimalAttribute(), new BigDecimal(attr.get(BIG_DECIMAL_ATTRIBUTE).n())); + assertNumericSetsEquals(x.getIntegerAttribute(), attr.get(INTEGER_ATTRIBUTE).ns()); + assertEquals(x.getStringAttribute(), attr.get(STRING_ATTRIBUTE).s()); + assertSetsEqual(x.getStringSetAttribute(), toSet(attr.get(STRING_SET_ATTRIBUTE).ss())); + } + } + + private RangeKeyClass newRangeKey(long hashKey, double rangeKey) { + RangeKeyClass obj = new RangeKeyClass(); + obj.setKey(hashKey); + obj.setRangeKey(rangeKey); + return obj; + } + + @Test + public void testSave() throws Exception { + List objs = new ArrayList(); + for ( int i = 0; i < 5; i++ ) { + RangeKeyClass obj = getUniqueObject(); + objs.add(obj); + } + + DynamoDBMapper util = new DynamoDBMapper(dynamo); + for (RangeKeyClass obj : objs) { + util.save(obj); + } + + for (RangeKeyClass obj : objs) { + RangeKeyClass loaded = util.load(RangeKeyClass.class, obj.getKey(), obj.getRangeKey()); + assertEquals(obj, loaded); + } + } + + @Test + public void testUpdate() throws Exception { + List objs = new ArrayList(); + for ( int i = 0; i < 5; i++ ) { + RangeKeyClass obj = getUniqueObject(); + objs.add(obj); + } + + DynamoDBMapper util = new DynamoDBMapper(dynamo); + for (RangeKeyClass obj : objs) { + util.save(obj); + } + + for ( RangeKeyClass obj : objs ) { + RangeKeyClass replacement = getUniqueObject(); + replacement.setKey(obj.getKey()); + replacement.setRangeKey(obj.getRangeKey()); + replacement.setVersion(obj.getVersion()); + util.save(replacement); + + RangeKeyClass loadedObject = util.load(RangeKeyClass.class, obj.getKey(), obj.getRangeKey()); + assertEquals(replacement, loadedObject); + + // If we try to update the old version, we should get an error + replacement.setVersion(replacement.getVersion() - 1); + try { + util.save(replacement); + fail("Should have thrown an exception"); + } catch ( Exception expected ) { + } + } + } + + private RangeKeyClass getUniqueObject() { + RangeKeyClass obj = new RangeKeyClass(); + obj.setKey(startKey++); + obj.setIntegerAttribute(toSet(start++, start++, start++)); + obj.setBigDecimalAttribute(new BigDecimal(startKey++)); + obj.setRangeKey(start++); + obj.setStringAttribute("" + startKey++); + obj.setStringSetAttribute(toSet("" + startKey++, "" + startKey++, "" + startKey++)); + return obj; + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/ScalarAttributeIntegrationTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/ScalarAttributeIntegrationTest.java new file mode 100644 index 000000000000..940e6d58a7cf --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/ScalarAttributeIntegrationTest.java @@ -0,0 +1,58 @@ +/* + * 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.mapper.dynamodb.mapper; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBScalarAttribute; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; +import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType; +import software.amazon.awssdk.mapper.dynamodb.pojos.AutoKeyAndVal; + +import java.util.UUID; + +import org.junit.Test; + +/** + * Status tests for {@code ScalarAttribute}. + */ +public class ScalarAttributeIntegrationTest extends AbstractKeyAndValIntegrationTestCase { + + /** + * Test with a non-null enum val. + */ + @Test + public void testMarshalling() { + final KeyAndBinaryUuid object = new KeyAndBinaryUuid(); + object.setVal(UUID.randomUUID()); + assertBeforeAndAfterChange(false, object); + } + + /** + * An object with an enumeration. + */ + @DynamoDBTable(tableName="aws-java-sdk-util") + public static class KeyAndBinaryUuid extends AutoKeyAndVal { + @DynamoDBScalarAttribute(type=ScalarAttributeType.B) + public UUID getVal() { + return super.getVal(); + } + @Override + public void setVal(final UUID val) { + super.setVal(val); + } + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/ScanTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/ScanTest.java new file mode 100644 index 000000000000..3fb16500bb90 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/ScanTest.java @@ -0,0 +1,276 @@ +/* + * 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.mapper.dynamodb.mapper; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import software.amazon.awssdk.mapper.dynamodb.LocalDynamoDBTestBase; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import org.junit.BeforeClass; +import org.junit.Test; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBScanExpression; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; +import software.amazon.awssdk.mapper.dynamodb.PaginatedParallelScanList; +import software.amazon.awssdk.mapper.dynamodb.PaginatedScanList; +import software.amazon.awssdk.mapper.dynamodb.ScanResultPage; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition; +import software.amazon.awssdk.services.dynamodb.model.ComparisonOperator; +import software.amazon.awssdk.services.dynamodb.model.Condition; +import software.amazon.awssdk.services.dynamodb.model.ConditionalOperator; +import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest; +import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement; +import software.amazon.awssdk.services.dynamodb.model.KeyType; +import software.amazon.awssdk.services.dynamodb.model.ProvisionedThroughput; +import software.amazon.awssdk.services.dynamodb.model.ResourceInUseException; +import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType; +/** + * Integration tests for the scan operation on DynamoDBMapper. + */ +public class ScanTest extends LocalDynamoDBTestBase { + + private static final String TABLE_NAME = "aws-java-sdk-util-scan"; + /** + * We set a small limit in order to test the behavior of PaginatedList + * when it could not load all the scan result in one batch. + */ + private static final int SCAN_LIMIT = 10; + private static final int PARALLEL_SCAN_SEGMENTS = 3; + private static DynamoDbClient dynamo; + + private static void createTestData() throws Exception { + DynamoDBMapper util = new DynamoDBMapper(dynamo); + for (int i = 0; i < 500; i++) { + util.save(new SimpleClass(Integer.toString(i), Integer.toString(i))); + } + } + + @BeforeClass + public static void setUpTestData() throws Exception { + dynamo = client(); + String keyName = "id"; + CreateTableRequest createTableRequest = CreateTableRequest.builder() + .tableName(TABLE_NAME) + .keySchema(KeySchemaElement.builder().attributeName(keyName).keyType(KeyType.HASH).build()) + .attributeDefinitions( + AttributeDefinition.builder().attributeName(keyName) + .attributeType(ScalarAttributeType.S).build()) + .provisionedThroughput(ProvisionedThroughput.builder().readCapacityUnits(10L) + .writeCapacityUnits(5L).build()) + .build(); + + if (createTableIfNotExists(createTableRequest)) { + dynamo.waiter().waitUntilTableExists(b -> b.tableName(TABLE_NAME)); + } + + createTestData(); + } + + private static boolean createTableIfNotExists(CreateTableRequest createTableRequest) { + try { + dynamo.createTable(createTableRequest); + return true; + } catch (ResourceInUseException e) { + // Table already exists. + return false; + } + } + + + @Test + public void testScan() throws Exception { + DynamoDBMapper util = new DynamoDBMapper(dynamo); + + DynamoDBScanExpression scanExpression = new DynamoDBScanExpression().withLimit(SCAN_LIMIT); + scanExpression.addFilterCondition("value", Condition.builder().comparisonOperator(ComparisonOperator.NOT_NULL).build()); + scanExpression.addFilterCondition("extraData", Condition.builder().comparisonOperator(ComparisonOperator.NOT_NULL).build()); + List list = util.scan(SimpleClass.class, scanExpression); + + int count = 0; + Iterator iterator = list.iterator(); + while (iterator.hasNext()) { + count++; + SimpleClass next = iterator.next(); + assertNotNull(next.getExtraData()); + assertNotNull(next.getValue()); + } + + int totalCount = util.count(SimpleClass.class, scanExpression); + + assertNotNull(list.get(totalCount / 2)); + assertEquals(totalCount, count); + assertEquals(totalCount, list.size()); + + assertTrue(list.contains(list.get(list.size() / 2))); + assertEquals(count, list.toArray().length); + } + + /** + * Tests scanning the table with AND/OR logic operator. + */ + @Test + public void testScanWithConditionalOperator() { + DynamoDBMapper mapper = new DynamoDBMapper(dynamo); + + Map scanFilter = new HashMap<>(); + scanFilter.put("value", Condition.builder().comparisonOperator(ComparisonOperator.NOT_NULL).build()); + scanFilter.put("non-existent-field", Condition.builder().comparisonOperator(ComparisonOperator.NOT_NULL).build()); + DynamoDBScanExpression scanExpression = new DynamoDBScanExpression() + .withLimit(SCAN_LIMIT) + .withScanFilter(scanFilter) + .withConditionalOperator(ConditionalOperator.AND); + + List andConditionResult = mapper.scan(SimpleClass.class, scanExpression); + assertTrue(andConditionResult.isEmpty()); + + List orConditionResult = mapper.scan(SimpleClass.class, + scanExpression.withConditionalOperator(ConditionalOperator.OR)); + assertFalse(orConditionResult.isEmpty()); + } + + @Test + public void testParallelScan() throws Exception { + DynamoDBMapper util = new DynamoDBMapper(dynamo); + + DynamoDBScanExpression scanExpression = new DynamoDBScanExpression().withLimit(SCAN_LIMIT); + scanExpression.addFilterCondition("value", Condition.builder().comparisonOperator(ComparisonOperator.NOT_NULL).build()); + scanExpression.addFilterCondition("extraData", Condition.builder().comparisonOperator(ComparisonOperator.NOT_NULL).build()); + + PaginatedParallelScanList parallelScanList = util.parallelScan(SimpleClass.class, scanExpression, PARALLEL_SCAN_SEGMENTS); + int count = 0; + Iterator iterator = parallelScanList.iterator(); + HashMap allDataAppearance = new HashMap(); + for (int i=0;i<500;i++) { + allDataAppearance.put("" + i, false); + } + while (iterator.hasNext()) { + count++; + SimpleClass next = iterator.next(); + assertNotNull(next.getExtraData()); + assertNotNull(next.getValue()); + allDataAppearance.put(next.getId(), true); + } + assertFalse(allDataAppearance.values().contains(false)); + + int totalCount = util.count(SimpleClass.class, scanExpression); + + assertNotNull(parallelScanList.get(totalCount / 2)); + assertEquals(totalCount, count); + assertEquals(totalCount, parallelScanList.size()); + + assertTrue(parallelScanList.contains(parallelScanList.get(parallelScanList.size() / 2))); + assertEquals(count, parallelScanList.toArray().length); + + } + + @Test + public void testScanPage() throws Exception { + DynamoDBMapper util = new DynamoDBMapper(dynamo); + + DynamoDBScanExpression scanExpression = new DynamoDBScanExpression(); + scanExpression.addFilterCondition("value", + Condition.builder().comparisonOperator(ComparisonOperator.NOT_NULL).build()); + scanExpression.addFilterCondition("extraData", + Condition.builder().comparisonOperator(ComparisonOperator.NOT_NULL).build()); + int limit = 3; + scanExpression.setLimit(limit); + ScanResultPage result = util.scanPage(SimpleClass.class, scanExpression); + + int count = 0; + Iterator iterator = result.getResults().iterator(); + Set seen = new HashSet(); + while ( iterator.hasNext() ) { + count++; + SimpleClass next = iterator.next(); + assertNotNull(next.getExtraData()); + assertNotNull(next.getValue()); + assertTrue(seen.add(next)); + } + + assertEquals(limit, count); + assertEquals(count, result.getResults().toArray().length); + + scanExpression.setExclusiveStartKey(result.getLastEvaluatedKey()); + result = util.scanPage(SimpleClass.class, scanExpression); + + iterator = result.getResults().iterator(); + count = 0; + while ( iterator.hasNext() ) { + count++; + SimpleClass next = iterator.next(); + assertNotNull(next.getExtraData()); + assertNotNull(next.getValue()); + assertTrue(seen.add(next)); + } + + assertEquals(limit, count); + assertEquals(count, result.getResults().toArray().length); + + } + @DynamoDBTable(tableName = "aws-java-sdk-util-scan") + public static final class SimpleClass { + private String id; + private String value; + private String extraData; + + + public SimpleClass() {} + + public SimpleClass(String id, String value) { + this.id = id; + this.value = value; + this.extraData = UUID.randomUUID().toString(); + } + + @DynamoDBHashKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getExtraData() { + return extraData; + } + + public void setExtraData(String extraData) { + this.extraData = extraData; + } + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/SimpleNumericAttributesIntegrationTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/SimpleNumericAttributesIntegrationTest.java new file mode 100644 index 000000000000..e25f0f18b53f --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/SimpleNumericAttributesIntegrationTest.java @@ -0,0 +1,278 @@ +/* + * 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.mapper.dynamodb.mapper; + +import static org.junit.Assert.*; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.junit.BeforeClass; +import org.junit.Test; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperIntegrationTestBase; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; +import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; +import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; + +/** + * Tests numeric attributes + */ +public class SimpleNumericAttributesIntegrationTest extends DynamoDBMapperIntegrationTestBase { + + private static final String INT_ATTRIBUTE = "intAttribute"; + private static final String INTEGER_ATTRIBUTE = "integerAttribute"; + private static final String FLOAT_ATTRIBUTE = "floatAttribute"; + private static final String FLOAT_OBJECT_ATTRIBUTE = "floatObjectAttribute"; + private static final String DOUBLE_ATTRIBUTE = "doubleAttribute"; + private static final String DOUBLE_OBJECT_ATTRIBUTE = "doubleObjectAttribute"; + private static final String BIG_INTEGER_ATTRIBUTE = "bigIntegerAttribute"; + private static final String BIG_DECIMAL_ATTRIBUTE = "bigDecimalAttribute"; + private static final String LONG_ATTRIBUTE = "longAttribute"; + private static final String LONG_OBJECT_ATTRIBUTE = "longObjectAttribute"; + private static final String BYTE_ATTRIBUTE = "byteAttribute"; + private static final String BYTE_OBJECT_ATTRIBUTE = "byteObjectAttribute"; + private static final String BOOLEAN_ATTRIBUTE = "booleanAttribute"; + private static final String BOOLEAN_OBJECT_ATTRIBUTE = "booleanObjectAttribute"; + private static final String SHORT_ATTRIBUTE = "shortAttribute"; + private static final String SHORT_OBJECT_ATTRIBUTE = "shortObjectAttribute"; + + // We don't start with the current system millis like other tests because + // it's out of the range of some data types + private static int start = 1; + private static int byteStart = -127; + + private static final List> attrs = new LinkedList>(); + + // Test data + static { + for ( int i = 0; i < 5; i++ ) { + Map attr = new HashMap(); + attr.put(KEY_NAME, AttributeValue.builder().s("" + start++).build()); + attr.put(INT_ATTRIBUTE, AttributeValue.builder().n("" + start++).build()); + attr.put(INTEGER_ATTRIBUTE, AttributeValue.builder().n("" + start++).build()); + attr.put(FLOAT_ATTRIBUTE, AttributeValue.builder().n("" + start++).build()); + attr.put(FLOAT_OBJECT_ATTRIBUTE, AttributeValue.builder().n("" + start++).build()); + attr.put(DOUBLE_ATTRIBUTE, AttributeValue.builder().n("" + start++).build()); + attr.put(DOUBLE_OBJECT_ATTRIBUTE, AttributeValue.builder().n("" + start++).build()); + attr.put(BIG_INTEGER_ATTRIBUTE, AttributeValue.builder().n("" + start++).build()); + attr.put(BIG_DECIMAL_ATTRIBUTE, AttributeValue.builder().n("" + start++).build()); + attr.put(LONG_ATTRIBUTE, AttributeValue.builder().n("" + start++).build()); + attr.put(LONG_OBJECT_ATTRIBUTE, AttributeValue.builder().n("" + start++).build()); + attr.put(BYTE_ATTRIBUTE, AttributeValue.builder().n("" + byteStart++).build()); + attr.put(BYTE_OBJECT_ATTRIBUTE, AttributeValue.builder().n("" + byteStart++).build()); + attr.put(BOOLEAN_ATTRIBUTE, AttributeValue.builder().n(start++ % 2 == 0 ? "1" : "0").build()); + attr.put(BOOLEAN_OBJECT_ATTRIBUTE, AttributeValue.builder().n(start++ % 2 == 0 ? "1" : "0").build()); + attr.put(SHORT_ATTRIBUTE, AttributeValue.builder().n("" + byteStart++).build()); + attr.put(SHORT_OBJECT_ATTRIBUTE, AttributeValue.builder().n("" + byteStart++).build()); + attrs.add(attr); + } + }; + + @BeforeClass + public static void setUp() throws Exception { + DynamoDBMapperIntegrationTestBase.setUp(); + + // Insert the data + for ( Map attr : attrs ) { + dynamo.putItem(PutItemRequest.builder().tableName(TABLE_NAME).item(attr).build()); + } + } + + private NumberAttributeClass getKeyObject(String key) { + NumberAttributeClass obj = new NumberAttributeClass(); + obj.setKey(key); + return obj; + } + + @Test + public void testLoad() throws Exception { + DynamoDBMapper util = new DynamoDBMapper(dynamo); + + for ( Map attr : attrs ) { + NumberAttributeClass x = util.load(getKeyObject(attr.get(KEY_NAME).s())); + assertEquals(x.getKey(), attr.get(KEY_NAME).s()); + + // Convert all numbers to the most inclusive type for easy comparison + assertEquals(x.getBigDecimalAttribute(), new BigDecimal(attr.get(BIG_DECIMAL_ATTRIBUTE).n())); + assertEquals(new BigDecimal(x.getBigIntegerAttribute()), new BigDecimal(attr.get(BIG_INTEGER_ATTRIBUTE).n())); + assertEquals(new BigDecimal(x.getFloatAttribute()), new BigDecimal(attr.get(FLOAT_ATTRIBUTE).n())); + assertEquals(new BigDecimal(x.getFloatObjectAttribute()), new BigDecimal(attr.get(FLOAT_OBJECT_ATTRIBUTE).n())); + assertEquals(new BigDecimal(x.getDoubleAttribute()), new BigDecimal(attr.get(DOUBLE_ATTRIBUTE).n())); + assertEquals(new BigDecimal(x.getDoubleObjectAttribute()), new BigDecimal(attr.get(DOUBLE_OBJECT_ATTRIBUTE).n())); + assertEquals(new BigDecimal(x.getIntAttribute()), new BigDecimal(attr.get(INT_ATTRIBUTE).n())); + assertEquals(new BigDecimal(x.getIntegerAttribute()), new BigDecimal(attr.get(INTEGER_ATTRIBUTE).n())); + assertEquals(new BigDecimal(x.getLongAttribute()), new BigDecimal(attr.get(LONG_ATTRIBUTE).n())); + assertEquals(new BigDecimal(x.getLongObjectAttribute()), new BigDecimal(attr.get(LONG_OBJECT_ATTRIBUTE).n())); + assertEquals(new BigDecimal(x.getByteAttribute()), new BigDecimal(attr.get(BYTE_ATTRIBUTE).n())); + assertEquals(new BigDecimal(x.getByteObjectAttribute()), new BigDecimal(attr.get(BYTE_OBJECT_ATTRIBUTE).n())); + assertEquals(new BigDecimal(x.getShortAttribute()), new BigDecimal(attr.get(SHORT_ATTRIBUTE).n())); + assertEquals(new BigDecimal(x.getShortObjectAttribute()), new BigDecimal(attr.get(SHORT_OBJECT_ATTRIBUTE).n())); + assertEquals(x.isBooleanAttribute(), attr.get(BOOLEAN_ATTRIBUTE).n().equals("1")); + assertEquals(x.getBooleanObjectAttribute(), attr.get(BOOLEAN_OBJECT_ATTRIBUTE).n().equals("1")); + } + + // Test loading an object that doesn't exist + assertNull(util.load(getKeyObject("does not exist"))); + } + + @Test + public void testSave() throws Exception { + List objs = new ArrayList(); + for ( int i = 0; i < 5; i++ ) { + NumberAttributeClass obj = getUniqueObject(); + objs.add(obj); + } + + DynamoDBMapper util = new DynamoDBMapper(dynamo); + for (NumberAttributeClass obj : objs) { + util.save(obj); + } + + for (NumberAttributeClass obj : objs) { + NumberAttributeClass loaded = util.load(obj); + loaded.setIgnored(obj.getIgnored()); + assertEquals(obj, loaded); + } + } + + @Test + public void testUpdate() throws Exception { + List objs = new ArrayList(); + for ( int i = 0; i < 5; i++ ) { + NumberAttributeClass obj = getUniqueObject(); + objs.add(obj); + } + + DynamoDBMapper util = new DynamoDBMapper(dynamo); + for (NumberAttributeClass obj : objs) { + util.save(obj); + } + + for ( NumberAttributeClass obj : objs ) { + NumberAttributeClass replacement = getUniqueObject(); + replacement.setKey(obj.getKey()); + util.save(replacement); + + NumberAttributeClass loadedObject = util.load(obj); + + // The ignored attribute isn't handled by the mapper, so we have to + // set it manually here before doing the comparison. + assertFalse(replacement.getIgnored().equals(loadedObject.getIgnored())); + loadedObject.setIgnored(replacement.getIgnored()); + assertEquals(replacement, loadedObject); + } + } + + /** + * Tests automatically setting a hash key upon saving. + */ + @Test + public void testSetHashKey() throws Exception { + List objs = new ArrayList(); + for ( int i = 0; i < 5; i++ ) { + NumberAttributeClass obj = getUniqueObject(); + obj.setKey(null); + objs.add(obj); + } + + DynamoDBMapper util = new DynamoDBMapper(dynamo); + for (NumberAttributeClass obj : objs) { + assertNull(obj.getKey()); + util.save(obj); + assertNotNull(obj.getKey()); + NumberAttributeClass loadedObject = util.load(obj); + + // The ignored attribute isn't handled by the mapper, so we have to + // set it manually here before doing the comparison. + assertFalse(obj.getIgnored().equals(loadedObject.getIgnored())); + loadedObject.setIgnored(obj.getIgnored()); + assertEquals(obj, loadedObject); + } + } + + @Test + public void testDelete() throws Exception { + NumberAttributeClass obj = getUniqueObject(); + DynamoDBMapper util = new DynamoDBMapper(dynamo); + util.save(obj); + + NumberAttributeClass loaded = util.load(NumberAttributeClass.class, obj.getKey()); + loaded.setIgnored(obj.getIgnored()); + assertEquals(obj, loaded); + + util.delete(obj); + assertNull(util.load(NumberAttributeClass.class, obj.getKey())); + } + + @Test + public void performanceTest() throws Exception { + NumberAttributeClass obj = getUniqueObject(); + DynamoDBMapper mapper = new DynamoDBMapper(dynamo); + mapper.save(obj); + + GetItemResponse item = dynamo.getItem(GetItemRequest.builder().tableName("aws-java-sdk-util").key( + getMapKey(KEY_NAME, AttributeValue.builder().s(obj.getKey()).build())).build()); + + long start = System.currentTimeMillis(); + for (int i = 0; i < 10000; i++) { + mapper.marshallIntoObject(NumberAttributeClass.class, item.item()); + } + + long end = System.currentTimeMillis(); + + System.err.println("time: " + (end - start)); + } + + private NumberAttributeClass getUniqueObject() { + NumberAttributeClass obj = new NumberAttributeClass(); + obj.setKey(String.valueOf(startKey++)); + obj.setBigDecimalAttribute(new BigDecimal(startKey++)); + obj.setBigIntegerAttribute(new BigInteger("" + startKey++)); + obj.setByteAttribute((byte)byteStart++); + obj.setByteObjectAttribute(new Byte("" + byteStart++)); + obj.setDoubleAttribute(new Double("" + start++)); + obj.setDoubleObjectAttribute(new Double("" + start++)); + obj.setFloatAttribute(new Float("" + start++)); + obj.setFloatObjectAttribute(new Float("" + start++)); + obj.setIntAttribute(new Integer("" + start++)); + obj.setIntegerAttribute(new Integer("" + start++)); + obj.setLongAttribute(new Long("" + start++)); + obj.setLongObjectAttribute(new Long("" + start++)); + obj.setShortAttribute(new Short("" + start++)); + obj.setShortObjectAttribute(new Short("" + start++)); + obj.setDateAttribute(new Date(startKey++)); + obj.setBooleanAttribute(start++ % 2 == 0); + obj.setBooleanObjectAttribute(start++ % 2 == 0); + obj.setIgnored("" + start++); + Calendar cal = GregorianCalendar.getInstance(); + cal.setTime(new Date(startKey++)); + obj.setCalendarAttribute(cal); + return obj; + } + + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/SimpleStringAttributesIntegrationTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/SimpleStringAttributesIntegrationTest.java new file mode 100644 index 000000000000..691050894d76 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/SimpleStringAttributesIntegrationTest.java @@ -0,0 +1,210 @@ +/* + * 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.mapper.dynamodb.mapper; + +import static org.junit.Assert.*; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.junit.BeforeClass; +import org.junit.Test; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperIntegrationTestBase; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.ConsistentReads; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.SaveBehavior; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; +import software.amazon.awssdk.mapper.dynamodb.pojos.StringAttributeClass; + +/** + * Tests simple string attributes + */ +public class SimpleStringAttributesIntegrationTest extends DynamoDBMapperIntegrationTestBase { + + private static final String ORIGINAL_NAME_ATTRIBUTE = "originalName"; + private static final String STRING_ATTRIBUTE = "stringAttribute"; + private static final List> attrs = new LinkedList>(); + + // Test data + static { + for ( int i = 0; i < 5; i++ ) { + Map attr = new HashMap(); + attr.put(KEY_NAME, AttributeValue.builder().s("" + startKey++).build()); + attr.put(STRING_ATTRIBUTE, AttributeValue.builder().s("" + startKey++).build()); + attr.put(ORIGINAL_NAME_ATTRIBUTE, AttributeValue.builder().s("" + startKey++).build()); + attrs.add(attr); + } + }; + + @BeforeClass + public static void setUp() throws Exception { + DynamoDBMapperIntegrationTestBase.setUp(); + + // Insert the data + for ( Map attr : attrs ) { + dynamo.putItem(PutItemRequest.builder().tableName(TABLE_NAME).item(attr).build()); + } + } + + @Test + public void testLoad() throws Exception { + DynamoDBMapper util = new DynamoDBMapper(dynamo); + + for ( Map attr : attrs ) { + StringAttributeClass x = util.load(StringAttributeClass.class, attr.get(KEY_NAME).s()); + assertEquals(x.getKey(), attr.get(KEY_NAME).s()); + assertEquals(x.getStringAttribute(), attr.get(STRING_ATTRIBUTE).s()); + assertEquals(x.getRenamedAttribute(), attr.get(ORIGINAL_NAME_ATTRIBUTE).s()); + } + + } + + @Test + public void testSave() { + List objs = new ArrayList(); + for ( int i = 0; i < 5; i++ ) { + StringAttributeClass obj = getUniqueObject(); + objs.add(obj); + } + + DynamoDBMapper util = new DynamoDBMapper(dynamo); + for (StringAttributeClass obj : objs) { + util.save(obj); + } + + for (StringAttributeClass obj : objs) { + StringAttributeClass loaded = util.load(StringAttributeClass.class, obj.getKey()); + assertEquals(obj, loaded); + } + } + + /** + * Tests saving an incomplete object into DynamoDB + */ + @Test + public void testIncompleteObject() { + StringAttributeClass obj = getUniqueObject(); + obj.setStringAttribute(null); + DynamoDBMapper util = new DynamoDBMapper(dynamo); + util.save(obj); + + assertEquals(obj, util.load(StringAttributeClass.class, obj.getKey())); + + // test removing an attribute + assertNotNull(obj.getRenamedAttribute()); + obj.setRenamedAttribute(null); + util.save(obj); + assertEquals(obj, util.load(StringAttributeClass.class, obj.getKey())); + } + + @Test + public void testUpdate() { + List objs = new ArrayList(); + for ( int i = 0; i < 5; i++ ) { + StringAttributeClass obj = getUniqueObject(); + objs.add(obj); + } + + DynamoDBMapper util = new DynamoDBMapper(dynamo); + for (StringAttributeClass obj : objs) { + util.save(obj); + } + + for ( StringAttributeClass obj : objs ) { + StringAttributeClass replacement = getUniqueObject(); + replacement.setKey(obj.getKey()); + util.save(replacement); + + assertEquals(replacement, util.load(StringAttributeClass.class, obj.getKey())); + } + } + + @Test + public void testSaveOnlyKey() { + KeyOnly obj = new KeyOnly(); + obj.setKey("" + startKey++); + DynamoDBMapper mapper = new DynamoDBMapper(dynamo); + mapper.save(obj); + + KeyOnly loaded = mapper.load(KeyOnly.class, obj.getKey(), new DynamoDBMapperConfig(ConsistentReads.CONSISTENT)); + assertEquals(obj, loaded); + + // saving again shouldn't be an error + mapper.save(obj); + } + + @Test + public void testSaveOnlyKeyClobber() { + KeyOnly obj = new KeyOnly(); + obj.setKey("" + startKey++); + DynamoDBMapper mapper = new DynamoDBMapper(dynamo); + mapper.save(obj, new DynamoDBMapperConfig(SaveBehavior.CLOBBER)); + + KeyOnly loaded = mapper.load(KeyOnly.class, obj.getKey(), new DynamoDBMapperConfig(ConsistentReads.CONSISTENT)); + assertEquals(obj, loaded); + + // saving again shouldn't be an error + mapper.save(obj, new DynamoDBMapperConfig(SaveBehavior.CLOBBER)); + } + + @DynamoDBTable(tableName="aws-java-sdk-util") + public static final class KeyOnly { + private String key; + + @DynamoDBHashKey + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) + return true; + if ( obj == null ) + return false; + if ( getClass() != obj.getClass() ) + return false; + KeyOnly other = (KeyOnly) obj; + if ( key == null ) { + if ( other.key != null ) + return false; + } else if ( !key.equals(other.key) ) + return false; + return true; + } + } + + private StringAttributeClass getUniqueObject() { + StringAttributeClass obj = new StringAttributeClass(); + obj.setKey(String.valueOf(startKey++)); + obj.setRenamedAttribute(String.valueOf(startKey++)); + obj.setStringAttribute(String.valueOf(startKey++)); + return obj; + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/StringSetAttributesIntegrationTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/StringSetAttributesIntegrationTest.java new file mode 100644 index 000000000000..ff3fe343c9ca --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/StringSetAttributesIntegrationTest.java @@ -0,0 +1,147 @@ +/* + * 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.mapper.dynamodb.mapper; + +import static org.junit.Assert.*; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.junit.BeforeClass; +import org.junit.Test; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperIntegrationTestBase; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; +import software.amazon.awssdk.mapper.dynamodb.pojos.StringSetAttributeClass; + + +/** + * Tests string set attributes + */ +public class StringSetAttributesIntegrationTest extends DynamoDBMapperIntegrationTestBase { + + private static final String ORIGINAL_NAME_ATTRIBUTE = "originalName"; + private static final String STRING_SET_ATTRIBUTE = "stringSetAttribute"; + private static final String EXTRA_ATTRIBUTE = "extra"; + private static final List> attrs = new LinkedList>(); + + // Test data + static { + for ( int i = 0; i < 5; i++ ) { + Map attr = new HashMap(); + attr.put(KEY_NAME, AttributeValue.builder().s("" + startKey++).build()); + attr.put(STRING_SET_ATTRIBUTE, AttributeValue.builder().ss("" + ++startKey, "" + ++startKey, "" + ++startKey).build()); + attr.put(ORIGINAL_NAME_ATTRIBUTE, AttributeValue.builder().ss("" + ++startKey, "" + ++startKey, "" + ++startKey).build()); + attr.put(EXTRA_ATTRIBUTE, AttributeValue.builder().ss("" + ++startKey, "" + ++startKey, "" + ++startKey).build()); + attrs.add(attr); + } + }; + + @BeforeClass + public static void setUp() throws Exception { + DynamoDBMapperIntegrationTestBase.setUp(); + + // Insert the data + for ( Map attr : attrs ) { + dynamo.putItem(PutItemRequest.builder().tableName(TABLE_NAME).item(attr).build()); + } + } + + @Test + public void testLoad() throws Exception { + DynamoDBMapper util = new DynamoDBMapper(dynamo); + + for ( Map attr : attrs ) { + StringSetAttributeClass x = util.load(StringSetAttributeClass.class, attr.get(KEY_NAME).s()); + assertEquals(x.getKey(), attr.get(KEY_NAME).s()); + assertSetsEqual(x.getStringSetAttribute(), toSet(attr.get(STRING_SET_ATTRIBUTE).ss())); + assertSetsEqual(x.getStringSetAttributeRenamed(), toSet(attr.get(ORIGINAL_NAME_ATTRIBUTE).ss())); + } + } + + /** + * Tests saving only some attributes of an object. + */ + @Test + public void testIncompleteObject() { + DynamoDBMapper util = new DynamoDBMapper(dynamo); + + StringSetAttributeClass obj = getUniqueObject(); + obj.setStringSetAttribute(null); + util.save(obj); + + assertEquals(obj, util.load(StringSetAttributeClass.class, obj.getKey())); + + obj.setStringSetAttributeRenamed(null); + util.save(obj); + assertEquals(obj, util.load(StringSetAttributeClass.class, obj.getKey())); + } + + @Test + public void testSave() throws Exception { + List objs = new ArrayList(); + for ( int i = 0; i < 5; i++ ) { + StringSetAttributeClass obj = getUniqueObject(); + objs.add(obj); + } + + DynamoDBMapper util = new DynamoDBMapper(dynamo); + for (StringSetAttributeClass obj : objs) { + util.save(obj); + } + + for (StringSetAttributeClass obj : objs) { + StringSetAttributeClass loaded = util.load(StringSetAttributeClass.class, obj.getKey()); + assertEquals(obj, loaded); + } + } + + @Test + public void testUpdate() throws Exception { + List objs = new ArrayList(); + for ( int i = 0; i < 5; i++ ) { + StringSetAttributeClass obj = getUniqueObject(); + objs.add(obj); + } + + DynamoDBMapper util = new DynamoDBMapper(dynamo); + for (StringSetAttributeClass obj : objs) { + util.save(obj); + } + + for ( StringSetAttributeClass obj : objs ) { + StringSetAttributeClass replacement = getUniqueObject(); + replacement.setKey(obj.getKey()); + util.save(replacement); + + assertEquals(replacement, util.load(StringSetAttributeClass.class, obj.getKey())); + } + } + + private StringSetAttributeClass getUniqueObject() { + StringSetAttributeClass obj = new StringSetAttributeClass(); + obj.setKey(String.valueOf(startKey++)); + obj.setStringSetAttribute(toSet(String.valueOf(startKey++), String.valueOf(startKey++), String.valueOf(startKey++))); + obj.setStringSetAttributeRenamed(toSet(String.valueOf(startKey++), String.valueOf(startKey++), String.valueOf(startKey++))); + return obj; + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TableMapperIntegrationTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TableMapperIntegrationTest.java new file mode 100644 index 000000000000..c31b09e7ad06 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TableMapperIntegrationTest.java @@ -0,0 +1,446 @@ +/* + * 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.mapper.dynamodb.mapper; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAttribute; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAutoGeneratedTimestamp; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBQueryExpression; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTableMapper; +import software.amazon.awssdk.mapper.dynamodb.pojos.AutoKeyAndVal; +import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import org.junit.Test; + +/** + * Tests updating component attribute fields correctly. + */ +public class TableMapperIntegrationTest extends AbstractKeyAndValIntegrationTestCase { + + /** + * Test using {@code Date}. + */ + @Test + public void testSaveIfNotExists() { + final DynamoDBTableMapper mapper = util.newTableMapper(KeyAndDateValue.class); + + final KeyAndDateValue object = new KeyAndDateValue(); + + mapper.saveIfNotExists(object); + } + + /** + * Test using {@code Date}. + */ + @Test(expected=ConditionalCheckFailedException.class) + public void testSaveIfNotExistsWhenExists() { + final DynamoDBTableMapper mapper = util.newTableMapper(KeyAndDateValue.class); + + final KeyAndDateValue object = new KeyAndDateValue(); + + mapper.saveIfNotExists(object); + mapper.saveIfNotExists(object); + } + + /** + * Test using {@code Date}. + */ + @Test + public void testSaveWhenExists() { + final DynamoDBTableMapper mapper = util.newTableMapper(KeyAndDateValue.class); + + final KeyAndDateValue object = new KeyAndDateValue(); + + mapper.saveIfNotExists(object); + mapper.save(object); + } + /** + * Test using {@code Date}. + */ + @Test(expected=ConditionalCheckFailedException.class) + public void testSaveIfExistsWhenNotExists() { + final DynamoDBTableMapper mapper = util.newTableMapper(KeyAndDateValue.class); + + final KeyAndDateValue object = new KeyAndDateValue(); + object.setKey(UUID.randomUUID().toString()); + + mapper.saveIfExists(object); + } + + /** + * Test using {@code Date}. + */ + @Test + public void testDeleteIfExistsWhenExists() { + final DynamoDBTableMapper mapper = util.newTableMapper(KeyAndDateValue.class); + + final KeyAndDateValue object = new KeyAndDateValue(); + + mapper.saveIfNotExists(object); + mapper.deleteIfExists(object); + } + + /** + * Test using {@code Date}. + */ + @Test(expected=ConditionalCheckFailedException.class) + public void testDeleteIfExistsWhenNotExists() { + final DynamoDBTableMapper mapper = util.newTableMapper(KeyAndDateValue.class); + + final KeyAndDateValue object = new KeyAndDateValue(); + object.setKey(UUID.randomUUID().toString()); + + mapper.deleteIfExists(object); + } + + /** + * Test batch load with no results. + */ + @Test + public void testBatchLoadItemList() { + final DynamoDBTableMapper mapper = util.newTableMapper(KeyAndDateValue.class); + + final KeyAndDateValue object1 = new KeyAndDateValue(); + final KeyAndDateValue object2 = new KeyAndDateValue(); + + assertEquals(0, mapper.batchSave(Arrays.asList(object1, object2)).size()); + assertEquals(2, mapper.batchLoad(Arrays.asList(object1, object2)).size()); + assertEquals(0, mapper.batchDelete(Arrays.asList(object1, object2)).size()); + assertEquals(0, mapper.batchLoad(Arrays.asList(object1, object2)).size()); + } + + /** + * Test batch load with no results. + */ + @Test + public void testBatchLoadItemListOnNull() { + final DynamoDBTableMapper mapper = util.newTableMapper(KeyAndDateValue.class); + + assertEquals(0, mapper.batchLoad((List)null).size()); + } + + /** + * Test batch load with no results. + */ + @Test + public void testBatchLoadItemListOnEmpty() { + final DynamoDBTableMapper mapper = util.newTableMapper(KeyAndDateValue.class); + + assertEquals(0, mapper.batchLoad(Collections.emptyList()).size()); + } + + /** + * Test a query. + */ + @Test + public void testQueryCount() { + final DynamoDBTableMapper mapper = util.newTableMapper(KeyAndDateValue.class); + + final KeyAndDateValue object = new KeyAndDateValue(); + object.setQueryDate(new Date()); + + mapper.saveIfNotExists(object); + + assertEquals(1, mapper.count(new DynamoDBQueryExpression() + .withHashKeyValues(object).withConsistentRead(true))); + } + + /** + * Test a query. + */ + @Test + public void testQueryBeginsWith() { + final DynamoDBTableMapper mapper = util.newTableMapper(KeyAndDateValue.class); + + final KeyAndDateValue object = new KeyAndDateValue(); + object.setQueryDate(new Date()); + + mapper.saveIfNotExists(object); + + assertEquals(1, mapper.queryPage(new DynamoDBQueryExpression() + .withHashKeyValues(object).withConsistentRead(true) + .withQueryFilterEntry("queryDate", mapper.field("queryDate").beginsWith(object.getQueryDate())) + ).getResults().size()); + } + + /** + * Test a query. + */ + @Test + public void testQueryBetween() { + final DynamoDBTableMapper mapper = util.newTableMapper(KeyAndDateValue.class); + + final KeyAndDateValue object = new KeyAndDateValue(); + object.setQueryDate(new Date()); + + mapper.saveIfNotExists(object); + + assertEquals(1, mapper.queryPage(new DynamoDBQueryExpression() + .withHashKeyValues(object).withConsistentRead(true) + .withQueryFilterEntry("queryDate", mapper.field("queryDate").between(object.getQueryDate(), object.getQueryDate())) + ).getResults().size()); + } + + /** + * Test a query. + */ + @Test + public void testQueryGreaterThanOrEqualTo() { + final DynamoDBTableMapper mapper = util.newTableMapper(KeyAndDateValue.class); + + final KeyAndDateValue object = new KeyAndDateValue(); + object.setQueryDate(new Date()); + + mapper.saveIfNotExists(object); + + assertEquals(1, mapper.queryPage(new DynamoDBQueryExpression() + .withHashKeyValues(object).withConsistentRead(true) + .withQueryFilterEntry("queryDate", mapper.field("queryDate").ge(object.getQueryDate())) + ).getResults().size()); + } + + /** + * Test a query. + */ + @Test + public void testQueryGreaterThan() { + final DynamoDBTableMapper mapper = util.newTableMapper(KeyAndDateValue.class); + + final KeyAndDateValue object = new KeyAndDateValue(); + object.setQueryDate(new Date()); + + mapper.saveIfNotExists(object); + + assertEquals(0, mapper.queryPage(new DynamoDBQueryExpression() + .withHashKeyValues(object).withConsistentRead(true) + .withQueryFilterEntry("queryDate", mapper.field("queryDate").gt(object.getQueryDate())) + ).getResults().size()); + } + + /** + * Test a query. + */ + @Test + public void testQueryEqualTo() { + final DynamoDBTableMapper mapper = util.newTableMapper(KeyAndDateValue.class); + + final KeyAndDateValue object = new KeyAndDateValue(); + object.setQueryDate(new Date()); + + mapper.saveIfNotExists(object); + + assertEquals(1, mapper.queryPage(new DynamoDBQueryExpression() + .withHashKeyValues(object).withConsistentRead(true) + .withQueryFilterEntry("queryDate", mapper.field("queryDate").eq(object.getQueryDate())) + ).getResults().size()); + } + + /** + * Test a query. + */ + @Test + public void testQueryIn() { + final DynamoDBTableMapper mapper = util.newTableMapper(KeyAndDateValue.class); + + final KeyAndDateValue object = new KeyAndDateValue(); + object.setQueryDate(new Date()); + + mapper.saveIfNotExists(object); + + assertEquals(1, mapper.queryPage(new DynamoDBQueryExpression() + .withHashKeyValues(object).withConsistentRead(true) + .withQueryFilterEntry("queryDate", mapper.field("queryDate").in(object.getQueryDate())) + ).getResults().size()); + } + + /** + * Test a query. + */ + @Test + public void testQueryIsNull() { + final DynamoDBTableMapper mapper = util.newTableMapper(KeyAndDateValue.class); + + final KeyAndDateValue object = new KeyAndDateValue(); + object.setQueryDate(new Date()); + + mapper.saveIfNotExists(object); + + assertEquals(0, mapper.queryPage(new DynamoDBQueryExpression() + .withHashKeyValues(object).withConsistentRead(true) + .withQueryFilterEntry("queryDate", mapper.field("queryDate").isNull()) + ).getResults().size()); + } + + /** + * Test a query. + */ + @Test + public void testQueryLessThanOrEqualTo() { + final DynamoDBTableMapper mapper = util.newTableMapper(KeyAndDateValue.class); + + final KeyAndDateValue object = new KeyAndDateValue(); + object.setQueryDate(new Date()); + + mapper.saveIfNotExists(object); + + assertEquals(1, mapper.queryPage(new DynamoDBQueryExpression() + .withHashKeyValues(object).withConsistentRead(true) + .withQueryFilterEntry("queryDate", mapper.field("queryDate").le(object.getQueryDate())) + ).getResults().size()); + } + + /** + * Test a query. + */ + @Test + public void testQueryLessThan() { + final DynamoDBTableMapper mapper = util.newTableMapper(KeyAndDateValue.class); + + final KeyAndDateValue object = new KeyAndDateValue(); + object.setQueryDate(new Date()); + + mapper.saveIfNotExists(object); + + assertEquals(0, mapper.queryPage(new DynamoDBQueryExpression() + .withHashKeyValues(object).withConsistentRead(true) + .withQueryFilterEntry("queryDate", mapper.field("queryDate").lt(object.getQueryDate())) + ).getResults().size()); + } + + /** + * Test a query. + */ + @Test + public void testQueryNotEqualTo() { + final DynamoDBTableMapper mapper = util.newTableMapper(KeyAndDateValue.class); + + final KeyAndDateValue object = new KeyAndDateValue(); + object.setQueryDate(new Date()); + + mapper.saveIfNotExists(object); + + assertEquals(0, mapper.queryPage(new DynamoDBQueryExpression() + .withHashKeyValues(object).withConsistentRead(true) + .withQueryFilterEntry("queryDate", mapper.field("queryDate").ne(object.getQueryDate())) + ).getResults().size()); + } + + /** + * Test a query. + */ + @Test + public void testQueryNotNull() { + final DynamoDBTableMapper mapper = util.newTableMapper(KeyAndDateValue.class); + + final KeyAndDateValue object = new KeyAndDateValue(); + object.setQueryDate(new Date()); + + mapper.saveIfNotExists(object); + + assertEquals(1, mapper.queryPage(new DynamoDBQueryExpression() + .withHashKeyValues(object).withConsistentRead(true) + .withQueryFilterEntry("queryDate", mapper.field("queryDate").notNull()) + ).getResults().size()); + } + + /** + * Test a query. + */ + @Test + public void testQueryAnyBetween() { + final DynamoDBTableMapper mapper = util.newTableMapper(KeyAndDateValue.class); + + final KeyAndDateValue object = new KeyAndDateValue(); + object.setQueryDate(new Date()); + + mapper.saveIfNotExists(object); + + assertEquals(1, mapper.queryPage(new DynamoDBQueryExpression() + .withHashKeyValues(object).withConsistentRead(true) + .withQueryFilterEntry("queryDate", mapper.field("queryDate").betweenAny(object.getQueryDate(), object.getQueryDate())) + ).getResults().size()); + } + + /** + * Test a query. + */ + @Test + public void testQueryAnyBetweenLoNull() { + final DynamoDBTableMapper mapper = util.newTableMapper(KeyAndDateValue.class); + + final KeyAndDateValue object = new KeyAndDateValue(); + object.setQueryDate(new Date()); + + mapper.saveIfNotExists(object); + + assertEquals(1, mapper.queryPage(new DynamoDBQueryExpression() + .withHashKeyValues(object).withConsistentRead(true) + .withQueryFilterEntry("queryDate", mapper.field("queryDate").betweenAny(null, object.getQueryDate())) + ).getResults().size()); + } + + /** + * Test a query. + */ + @Test + public void testQueryAnyBetweenHiNull() { + final DynamoDBTableMapper mapper = util.newTableMapper(KeyAndDateValue.class); + + final KeyAndDateValue object = new KeyAndDateValue(); + object.setQueryDate(new Date()); + + mapper.saveIfNotExists(object); + + assertEquals(1, mapper.queryPage(new DynamoDBQueryExpression() + .withHashKeyValues(object).withConsistentRead(true) + .withQueryFilterEntry("queryDate", mapper.field("queryDate").betweenAny(object.getQueryDate(), null)) + ).getResults().size()); + } + + /** + * An object with {@code Date}. + */ + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class KeyAndDateValue extends AutoKeyAndVal { + private Date queryDate; + @DynamoDBAutoGeneratedTimestamp + public Date getVal() { + return super.getVal(); + } + @Override + public void setVal(final Date val) { + super.setVal(val); + } + @DynamoDBAttribute(attributeName="queryDate") + public Date getQueryDate() { + return this.queryDate; + } + public void setQueryDate(final Date queryDate) { + this.queryDate = queryDate; + } + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TransactionLoadExpressionTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TransactionLoadExpressionTest.java new file mode 100644 index 000000000000..668f11e3700d --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TransactionLoadExpressionTest.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.mapper.dynamodb.mapper; + +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.createMultipleObjectTestDataForTransactionLoad; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.Existence; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.ProjectionExpressionMode; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.TransactionLoadTestData; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.TransactionLoadTestDataRequest; +import static org.junit.Assert.assertEquals; + +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import org.junit.Test; + +public class TransactionLoadExpressionTest extends TransactionsTestBase { + + @Test + public void testWithOneLoadExpression() { + TransactionLoadTestDataRequest testDataRequest = new TransactionLoadTestDataRequest().withNumObjects(6) + .withExistence(Existence.SOME) + .withUseSameObjectType(false) + .withProjectionExpressionMode(ProjectionExpressionMode.ONE); + TransactionLoadTestData testData = createMultipleObjectTestDataForTransactionLoad(dynamoMapper, testDataRequest); + + executeAndValidateTransactionLoad(testData.getTransactionLoadRequest(), testData.getExpectedObjects()); + } + + @Test + public void testWithMultipleLoadExpressions() { + TransactionLoadTestDataRequest testDataRequest = new TransactionLoadTestDataRequest().withNumObjects(6) + .withExistence(Existence.SOME) + .withUseSameObjectType(false) + .withProjectionExpressionMode(ProjectionExpressionMode.MULTIPLE); + TransactionLoadTestData testData = createMultipleObjectTestDataForTransactionLoad(dynamoMapper, testDataRequest); + + executeAndValidateTransactionLoad(testData.getTransactionLoadRequest(), testData.getExpectedObjects()); + } + + @Test + public void testWithMultipleLoadExpressionsButOnlyOneHavingAttributeNames() { + TransactionLoadTestDataRequest testDataRequest = new TransactionLoadTestDataRequest().withNumObjects(6) + .withExistence(Existence.SOME) + .withUseSameObjectType(false) + .withProjectionExpressionMode(ProjectionExpressionMode.ONE_ATTRIBUTE_NAMES); + TransactionLoadTestData testData = createMultipleObjectTestDataForTransactionLoad(dynamoMapper, testDataRequest); + + executeAndValidateTransactionLoad(testData.getTransactionLoadRequest(), testData.getExpectedObjects()); + } + + @Test + public void testWithMultipleLoadExpressionsHavingAttributeNames() { + TransactionLoadTestDataRequest testDataRequest = new TransactionLoadTestDataRequest().withNumObjects(6) + .withExistence(Existence.SOME) + .withUseSameObjectType(false) + .withProjectionExpressionMode(ProjectionExpressionMode.MULTIPLE_ATTRIBUTE_NAMES); + TransactionLoadTestData testData = createMultipleObjectTestDataForTransactionLoad(dynamoMapper, testDataRequest); + + executeAndValidateTransactionLoad(testData.getTransactionLoadRequest(), testData.getExpectedObjects()); + } + + @Test + public void testWithOneLoadExpressionContainingReservedWord() { + TransactionLoadTestDataRequest testDataRequest = new TransactionLoadTestDataRequest().withNumObjects(6) + .withExistence(Existence.SOME) + .withUseSameObjectType(false) + .withProjectionExpressionMode(ProjectionExpressionMode.ONE_RESERVED_WORD); + TransactionLoadTestData testData = createMultipleObjectTestDataForTransactionLoad(dynamoMapper, testDataRequest); + + try { + executeAndValidateTransactionLoad(testData.getTransactionLoadRequest(), testData.getExpectedObjects()); + } catch (AwsServiceException ex) { + assertEquals("ValidationException", ex.awsErrorDetails().errorCode()); + } + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TransactionLoadMixedTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TransactionLoadMixedTest.java new file mode 100644 index 000000000000..e515074c6d0b --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TransactionLoadMixedTest.java @@ -0,0 +1,503 @@ +/* + * 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.mapper.dynamodb.mapper; + +import static software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.TableNameOverride; +import static software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.TableNameResolver; +import static software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.ObjectTableNameResolver; +import static software.amazon.awssdk.mapper.dynamodb.pojos.CustomBooleanClass.CustomBoolean; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.createAllSupportedAnnotationsTable; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.createMultipleObjectTestDataForTransactionLoad; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.createSdkMapperSaveConfigTable; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.createSdkRangeTestTable; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.createSdkStringRangeTable; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.createStringHashKeyTable; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.deleteTestTable; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.Existence; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.getAllSupportedDataTypesObject; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.getAllSupportedAnnotationsObject; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.getCustomBooleanClassObject; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.getSchemaViolatingTestItemObject; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.getStringAttributeClass; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.getStringHashAndRangeKeyObjectWithKeys; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.ObjectSupplier; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.TransactionLoadTestData; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.TransactionLoadTestDataRequest; +import static org.junit.Assert.fail; +import static org.junit.Assert.assertEquals; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMappingException; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTypeConverter; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTypeConverterFactory; +import software.amazon.awssdk.mapper.dynamodb.TransactionLoadRequest; +import software.amazon.awssdk.mapper.dynamodb.pojos.AllSupportedAnnotationsClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.AllSupportedDataTypesClass; +import software.amazon.awssdk.services.dynamodb.model.ResourceNotFoundException; +import software.amazon.awssdk.services.dynamodb.model.TransactionCanceledException; +import software.amazon.awssdk.mapper.dynamodb.pojos.HashKeyRangeKeyBothAutoGenerated; +import software.amazon.awssdk.mapper.dynamodb.pojos.SchemaViolatingTestItem; +import software.amazon.awssdk.mapper.dynamodb.pojos.StringAttributeClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.TestItem; +import software.amazon.awssdk.mapper.dynamodb.pojos.RangeKeyClass; +import org.junit.Test; + +import java.util.List; +import java.util.UUID; + +public class TransactionLoadMixedTest extends TransactionsTestBase { + + private static final String stringAttributeSubClassTableName = "sdk-util-bfd571e3-2c18-4da6-9c08-487f710802c3"; + private static final String nonExistingTableName = "nonExistingTable-d60c8995-842a-423d-9b88-8c9bde03ef27"; + + // TODO: Refactor tableNameResolvers to re-use code between multiple such instances in transactionLoad/transactionWrite integration tests + private static final TableNameResolver tableNameResolverForMissingKeyTests = new TableNameResolver() { + @Override + public String getTableName(Class clazz, DynamoDBMapperConfig config) { + if (StringAttributeClass.class.equals(clazz)) { + return sdkUtilTableName; + } + if (TestItem.class.equals(clazz)) { + return mapperSaveConfigTableName; + } + if (RangeKeyClass.class.equals(clazz)) { + return sdkRangeTableName; + } + if (HashKeyRangeKeyBothAutoGenerated.class.equals(clazz)) { + return "sdk-string-keys-" + tableNameSuffix; + } + throw new RuntimeException("Unable to resolve table name for input object type: " + clazz); + } + }; + private static final TableNameResolver tableNameResolverForMissingTableTests = new TableNameResolver() { + @Override + public String getTableName(Class clazz, DynamoDBMapperConfig config) { + if (StringAttributeClass.class.equals(clazz)) { + return sdkUtilTableName; + } + if (TestItem.class.equals(clazz)) { + return mapperSaveConfigTableName; + } + if (RangeKeyClass.class.equals(clazz)) { + return sdkRangeTableName; + } + if (NoSuchTableClass.class.equals(clazz) || NonExistingTableTestClass.class.equals(clazz)) { + return nonExistingTableName; + } + throw new RuntimeException("Unable to resolve table name for input object type: " + clazz); + } + }; + private static final TableNameResolver tableNameResolverForTransactionCanceledTests = new TableNameResolver() { + @Override + public String getTableName(Class clazz, DynamoDBMapperConfig config) { + if (StringAttributeClass.class.equals(clazz)) { + return sdkUtilTableName; + } + if (TestItem.class.equals(clazz) || SchemaViolatingTestItem.class.equals(clazz)) { + return mapperSaveConfigTableName; + } + if (RangeKeyClass.class.equals(clazz)) { + return sdkRangeTableName; + } + throw new RuntimeException("Unable to resolve table name for input object type: " + clazz); + } + }; + + @Test + public void testWithTableNameOverride() throws InterruptedException { + String overriddenTableName = "sdk-util-" + UUID.randomUUID().toString(); + DynamoDBMapperConfig config = DynamoDBMapperConfig.builder().withTableNameOverride(TableNameOverride.withTableNameReplacement(overriddenTableName)) + .build(); + DynamoDBMapper mapperWithTableNameOverride = new DynamoDBMapper(dynamo, config); + + try { + createStringHashKeyTable(dynamo, overriddenTableName); + TransactionLoadTestDataRequest testDataRequest = new TransactionLoadTestDataRequest().withNumObjects(5) + .withExistence(Existence.SOME) + .withUseSameObjectType(true); + TransactionLoadTestData testData = createMultipleObjectTestDataForTransactionLoad(mapperWithTableNameOverride, testDataRequest); + + executeAndValidateTransactionLoad(mapperWithTableNameOverride, testData.getTransactionLoadRequest(), testData.getExpectedObjects()); + } finally { + deleteTestTable(dynamo, overriddenTableName); + } + } + + @DynamoDBTable(tableName = stringAttributeSubClassTableName) + public static class StringAttributeSubClass extends StringAttributeClass { + + } + + // This test is needed because by default TransactionsIntegrationTestBase creates mapper with TableNameResolver + @Test + public void testWithoutAnyOverriddenConfig() throws InterruptedException { + DynamoDBMapper mapperWithoutConfig = new DynamoDBMapper(dynamo); + + try { + createStringHashKeyTable(dynamo, stringAttributeSubClassTableName); + + ObjectSupplier objectSupplier = new ObjectSupplier() { + @Override + public Object get() { + return getStringAttributeSubClassObject(); + } + }; + TransactionLoadTestDataRequest testDataRequest = new TransactionLoadTestDataRequest().withNumObjects(5) + .withExistence(Existence.SOME) + .withUseSameObjectType(true) + .withObjectSupplier(objectSupplier); + TransactionLoadTestData testData = createMultipleObjectTestDataForTransactionLoad(mapperWithoutConfig, testDataRequest); + + executeAndValidateTransactionLoad(mapperWithoutConfig, testData.getTransactionLoadRequest(), testData.getExpectedObjects()); + } finally { + deleteTestTable(dynamo, stringAttributeSubClassTableName); + } + } + + @Test + public void testWithObjectTableNameResolver() throws InterruptedException { + String uuid = UUID.randomUUID().toString(); + final String overriddenSdkUtilTableName = "sdk-util-" + uuid; + final String overriddenMapperSaveConfigTableName = "mapper-save-config-" + uuid; + final String overriddenSdkRangeTableName = "sdk-range-" + uuid; + DynamoDBMapperConfig config = DynamoDBMapperConfig.builder().withObjectTableNameResolver(new ObjectTableNameResolver() { + @Override + public String getTableName(Object object, DynamoDBMapperConfig config) { + if (StringAttributeClass.class.equals(object.getClass())) { + return overriddenSdkUtilTableName; + } + if (TestItem.class.equals(object.getClass())) { + return overriddenMapperSaveConfigTableName; + } + if (RangeKeyClass.class.equals(object.getClass())) { + return overriddenSdkRangeTableName; + } + throw new RuntimeException("Unable to resolve table name for input object type: " + object.getClass()); + } + }).build(); + DynamoDBMapper mapperWithObjectTableNameResolver = new DynamoDBMapper(dynamo, config); + + try { + createStringHashKeyTable(dynamo, overriddenSdkUtilTableName); + createSdkMapperSaveConfigTable(dynamo, overriddenMapperSaveConfigTableName); + createSdkRangeTestTable(dynamo, overriddenSdkRangeTableName); + TransactionLoadTestDataRequest testDataRequest = new TransactionLoadTestDataRequest().withNumObjects(6) + .withExistence(Existence.SOME) + .withUseSameObjectType(false); + TransactionLoadTestData testData = createMultipleObjectTestDataForTransactionLoad(mapperWithObjectTableNameResolver, testDataRequest); + + executeAndValidateTransactionLoad(mapperWithObjectTableNameResolver, testData.getTransactionLoadRequest(), testData.getExpectedObjects()); + } finally { + deleteTestTable(dynamo, overriddenSdkUtilTableName); + deleteTestTable(dynamo, overriddenMapperSaveConfigTableName); + deleteTestTable(dynamo, overriddenSdkRangeTableName); + } + } + + @Test + public void testWithTypeConverterFactory() throws InterruptedException { + DynamoDBTypeConverterFactory typeConverterFactory = DynamoDBTypeConverterFactory.standard().override() + .with(String.class, CustomBoolean.class, new DynamoDBTypeConverter() { + @Override + public String convert(CustomBoolean bool) { + return String.valueOf(bool.getValue()); + } + + @Override + public CustomBoolean unconvert(String string) { + return new CustomBoolean(Boolean.valueOf(string)); + } + } + ).build(); + String overriddenTableName = "custom-boolean-" + UUID.randomUUID().toString(); + DynamoDBMapperConfig config = DynamoDBMapperConfig.builder().withTypeConverterFactory(typeConverterFactory) + .withTableNameOverride(TableNameOverride.withTableNameReplacement(overriddenTableName)) + .build(); + DynamoDBMapper mapperWithTypeConverterFactory = new DynamoDBMapper(dynamo, config); + + try { + createStringHashKeyTable(dynamo, overriddenTableName); + + ObjectSupplier objectSupplier = new ObjectSupplier() { + @Override + public Object get() { + return getCustomBooleanClassObject(); + } + }; + TransactionLoadTestDataRequest testDataRequest = new TransactionLoadTestDataRequest().withNumObjects(5) + .withExistence(Existence.SOME) + .withUseSameObjectType(true) + .withObjectSupplier(objectSupplier); + TransactionLoadTestData testData = createMultipleObjectTestDataForTransactionLoad(mapperWithTypeConverterFactory, testDataRequest); + + executeAndValidateTransactionLoad(mapperWithTypeConverterFactory, testData.getTransactionLoadRequest(), testData.getExpectedObjects()); + } finally { + deleteTestTable(dynamo, overriddenTableName); + } + } + + @Test + public void testWithMissingHashKeyValueButValidRangeKeyValue() { + DynamoDBMapperConfig config = DynamoDBMapperConfig.builder().withTableNameResolver(tableNameResolverForMissingKeyTests) + .build(); + DynamoDBMapper mapperWithTableNameResolver = new DynamoDBMapper(dynamo, config); + try { + TransactionLoadTestDataRequest testDataRequest = new TransactionLoadTestDataRequest().withNumObjects(6) + .withExistence(Existence.SOME) + .withUseSameObjectType(false); + TransactionLoadTestData testData = createMultipleObjectTestDataForTransactionLoad(mapperWithTableNameResolver, testDataRequest); + HashKeyRangeKeyBothAutoGenerated objWithMissingHashKey = getStringHashAndRangeKeyObjectWithKeys(); + objWithMissingHashKey.setKey(null); + TransactionLoadRequest transactionLoadRequest = testData.getTransactionLoadRequest(); + transactionLoadRequest.addLoad(objWithMissingHashKey); + + executeAndValidateTransactionLoad(mapperWithTableNameResolver, transactionLoadRequest, testData.getExpectedObjects()); + fail("Expected DynamoDBMappingException but no exception thrown"); + } catch (DynamoDBMappingException ex) { + assertEquals("HashKeyRangeKeyBothAutoGenerated[key]; no HASH key value present", ex.getMessage()); + } + } + + @Test + public void testWithValidHashKeyValueButMissingRangeKeyValue() { + DynamoDBMapperConfig config = DynamoDBMapperConfig.builder().withTableNameResolver(tableNameResolverForMissingKeyTests) + .build(); + DynamoDBMapper mapperWithTableNameResolver = new DynamoDBMapper(dynamo, config); + try { + TransactionLoadTestDataRequest testDataRequest = new TransactionLoadTestDataRequest().withNumObjects(6) + .withExistence(Existence.SOME) + .withUseSameObjectType(false); + TransactionLoadTestData testData = createMultipleObjectTestDataForTransactionLoad(mapperWithTableNameResolver, testDataRequest); + HashKeyRangeKeyBothAutoGenerated objWithMissingHashKey = getStringHashAndRangeKeyObjectWithKeys(); + objWithMissingHashKey.setRangeKey(null); + TransactionLoadRequest transactionLoadRequest = testData.getTransactionLoadRequest(); + transactionLoadRequest.addLoad(objWithMissingHashKey); + + executeAndValidateTransactionLoad(mapperWithTableNameResolver, transactionLoadRequest, testData.getExpectedObjects()); + fail("Expected DynamoDBMappingException but no exception thrown"); + } catch (DynamoDBMappingException ex) { + assertEquals("HashKeyRangeKeyBothAutoGenerated[rangeKey]; no RANGE key value present", ex.getMessage()); + } + } + + @Test + public void testWithTableMissingForOneItem() { + DynamoDBMapperConfig config = DynamoDBMapperConfig.builder().withTableNameResolver(tableNameResolverForMissingTableTests) + .build(); + DynamoDBMapper mapperWithTableNameResolver = new DynamoDBMapper(dynamo, config); + try { + TransactionLoadTestDataRequest testDataRequest = new TransactionLoadTestDataRequest().withNumObjects(6) + .withExistence(Existence.SOME) + .withUseSameObjectType(false); + TransactionLoadTestData testData = createMultipleObjectTestDataForTransactionLoad(mapperWithTableNameResolver, testDataRequest); + NoSuchTableClass noSuchTableClassObj = new NoSuchTableClass(); + noSuchTableClassObj.setKey("someKey"); + TransactionLoadRequest transactionLoadRequest = testData.getTransactionLoadRequest(); + transactionLoadRequest.addLoad(noSuchTableClassObj); + + executeAndValidateTransactionLoad(mapperWithTableNameResolver, transactionLoadRequest, testData.getExpectedObjects()); + fail("Expected ResourceNotFoundException but no exception thrown"); + } catch (ResourceNotFoundException ex) { + // No action needed, test passes + } + } + + @DynamoDBTable(tableName = nonExistingTableName) + public static class NonExistingTableTestClass extends StringAttributeClass { + + } + + @Test + public void testWithTableMissingForMultipleItems() { + DynamoDBMapperConfig config = DynamoDBMapperConfig.builder().withTableNameResolver(tableNameResolverForMissingTableTests) + .build(); + DynamoDBMapper mapperWithTableNameResolver = new DynamoDBMapper(dynamo, config); + try { + TransactionLoadTestDataRequest testDataRequest = new TransactionLoadTestDataRequest().withNumObjects(6) + .withExistence(Existence.SOME) + .withUseSameObjectType(false); + TransactionLoadTestData testData = createMultipleObjectTestDataForTransactionLoad(mapperWithTableNameResolver, testDataRequest); + TransactionLoadRequest transactionLoadRequest = testData.getTransactionLoadRequest(); + NoSuchTableClass noSuchTableClassObj = new NoSuchTableClass(); + noSuchTableClassObj.setKey("someKey"); + transactionLoadRequest.addLoad(noSuchTableClassObj); + transactionLoadRequest.addLoad(getStringAttributeClass()); + NonExistingTableTestClass nonExistingTableTestClassObj = new NonExistingTableTestClass(); + nonExistingTableTestClassObj.setKey("someOtherKey"); + transactionLoadRequest.addLoad(nonExistingTableTestClassObj); + + executeAndValidateTransactionLoad(mapperWithTableNameResolver, transactionLoadRequest, testData.getExpectedObjects()); + fail("Expected ResourceNotFoundException but no exception thrown"); + } catch (ResourceNotFoundException ex) { + // No action needed, test passes + } + } + + @Test + public void testWithOneItemCausingTransactionCancelled() { + DynamoDBMapperConfig config = DynamoDBMapperConfig.builder().withTableNameResolver(tableNameResolverForTransactionCanceledTests) + .build(); + DynamoDBMapper mapperWithTableNameResolver = new DynamoDBMapper(dynamo, config); + int numObjects = 7; + try { + TransactionLoadTestDataRequest testDataRequest = new TransactionLoadTestDataRequest().withNumObjects(numObjects - 1) + .withExistence(Existence.SOME) + .withUseSameObjectType(false); + TransactionLoadTestData testData = createMultipleObjectTestDataForTransactionLoad(mapperWithTableNameResolver, testDataRequest); + TransactionLoadRequest transactionLoadRequest = testData.getTransactionLoadRequest(); + transactionLoadRequest.addLoad(getSchemaViolatingTestItemObject()); + + mapperWithTableNameResolver.transactionLoad(transactionLoadRequest); + fail("Expected TransactionCanceledException but no exception thrown"); + } catch (TransactionCanceledException ex) { + // Expected + } + } + + @Test + public void testWithMultipleItemsCausingTransactionCancelled() { + DynamoDBMapperConfig config = DynamoDBMapperConfig.builder().withTableNameResolver(tableNameResolverForTransactionCanceledTests) + .build(); + DynamoDBMapper mapperWithTableNameResolver = new DynamoDBMapper(dynamo, config); + int numObjects = 9; + try { + TransactionLoadTestDataRequest testDataRequest = new TransactionLoadTestDataRequest().withNumObjects(numObjects - 3) + .withExistence(Existence.SOME) + .withUseSameObjectType(false); + TransactionLoadTestData testData = createMultipleObjectTestDataForTransactionLoad(mapperWithTableNameResolver, testDataRequest); + TransactionLoadRequest transactionLoadRequest = testData.getTransactionLoadRequest(); + transactionLoadRequest.addLoad(getSchemaViolatingTestItemObject()); + transactionLoadRequest.addLoad(getStringAttributeClass()); + transactionLoadRequest.addLoad(getSchemaViolatingTestItemObject()); + + mapperWithTableNameResolver.transactionLoad(transactionLoadRequest); + fail("Expected TransactionCanceledException but no exception thrown"); + } catch (TransactionCanceledException ex) { + // Expected + } + } + + @Test + public void testWithObjectsHavingAllSupportedDataTypes() throws InterruptedException { + final String allSupportedTypesTableName = "all-supported-types-" + tableNameSuffix; + // TODO: Refactor tableNameResolver to re-use code between multiple such instances in transactionLoad/transactionWrite integration tests + TableNameResolver tableNameResolver = new TableNameResolver() { + @Override + public String getTableName(Class clazz, DynamoDBMapperConfig config) { + if (StringAttributeClass.class.equals(clazz)) { + return sdkUtilTableName; + } + if (TestItem.class.equals(clazz)) { + return mapperSaveConfigTableName; + } + if (RangeKeyClass.class.equals(clazz)) { + return sdkRangeTableName; + } + if (AllSupportedDataTypesClass.class.equals(clazz)) { + return allSupportedTypesTableName; + } + throw new RuntimeException("Unable to resolve table name for input object type: " + clazz); + } + }; + DynamoDBMapperConfig config = DynamoDBMapperConfig.builder().withTableNameResolver(tableNameResolver) + .build(); + DynamoDBMapper mapperWithTableNameResolver = new DynamoDBMapper(dynamo, config); + try { + createSdkStringRangeTable(dynamo, allSupportedTypesTableName); + TransactionLoadTestDataRequest testDataRequest = new TransactionLoadTestDataRequest().withNumObjects(6) + .withExistence(Existence.SOME) + .withUseSameObjectType(false); + TransactionLoadTestData testData = createMultipleObjectTestDataForTransactionLoad(mapperWithTableNameResolver, testDataRequest); + TransactionLoadRequest transactionLoadRequest = testData.getTransactionLoadRequest(); + List expectedObjects = testData.getExpectedObjects(); + // Add an existing AllSupportedDataTypesClass object to transactionLoad + AllSupportedDataTypesClass allSupportedDataTypesObject1 = getAllSupportedDataTypesObject(); + mapperWithTableNameResolver.save(allSupportedDataTypesObject1); + transactionLoadRequest.addLoad(allSupportedDataTypesObject1); + expectedObjects.add(allSupportedDataTypesObject1); + // Add a non-existing AllSupportedDataTypesClass object to transactionLoad + AllSupportedDataTypesClass allSupportedDataTypesObject2 = getAllSupportedDataTypesObject(); + transactionLoadRequest.addLoad(allSupportedDataTypesObject2); + expectedObjects.add(null); + + executeAndValidateTransactionLoad(mapperWithTableNameResolver, transactionLoadRequest, testData.getExpectedObjects()); + } finally { + deleteTestTable(dynamo, allSupportedTypesTableName); + } + } + + @Test + public void testWithObjectsHavingAllSupportedAnnotations() throws InterruptedException { + final String allSupportedAnnotationsTableName = "all-supported-annotations-" + tableNameSuffix; + // TODO: Refactor tableNameResolver to re-use code between multiple such instances in transactionLoad/transactionWrite integration tests + TableNameResolver tableNameResolver = new TableNameResolver() { + @Override + public String getTableName(Class clazz, DynamoDBMapperConfig config) { + if (StringAttributeClass.class.equals(clazz)) { + return sdkUtilTableName; + } + if (TestItem.class.equals(clazz)) { + return mapperSaveConfigTableName; + } + if (RangeKeyClass.class.equals(clazz)) { + return sdkRangeTableName; + } + if (AllSupportedAnnotationsClass.class.equals(clazz)) { + return allSupportedAnnotationsTableName; + } + throw new RuntimeException("Unable to resolve table name for input object type: " + clazz); + } + }; + DynamoDBMapperConfig config = DynamoDBMapperConfig.builder().withTableNameResolver(tableNameResolver) + .build(); + DynamoDBMapper mapperWithTableNameResolver = new DynamoDBMapper(dynamo, config); + try { + createAllSupportedAnnotationsTable(dynamo, allSupportedAnnotationsTableName); + TransactionLoadTestDataRequest testDataRequest = new TransactionLoadTestDataRequest().withNumObjects(6) + .withExistence(Existence.SOME) + .withUseSameObjectType(false); + TransactionLoadTestData testData = createMultipleObjectTestDataForTransactionLoad(mapperWithTableNameResolver, testDataRequest); + TransactionLoadRequest transactionLoadRequest = testData.getTransactionLoadRequest(); + List expectedObjects = testData.getExpectedObjects(); + // Add an existing AllSupportedAnnotationsClass object to transactionLoad + AllSupportedAnnotationsClass allSupportedAnnotationsObject1 = getAllSupportedAnnotationsObject(); + mapperWithTableNameResolver.save(allSupportedAnnotationsObject1); + // Ignored attribute value shouldn't change + allSupportedAnnotationsObject1.setIgnoredAttribute(AllSupportedAnnotationsClass.IGNORED_ATTRIBUTE_VALUE); + transactionLoadRequest.addLoad(allSupportedAnnotationsObject1); + expectedObjects.add(allSupportedAnnotationsObject1); + // Add a non-existing AllSupportedAnnotationsClass object to transactionLoad + AllSupportedAnnotationsClass allSupportedAnnotationsObject2 = getAllSupportedAnnotationsObject(); + transactionLoadRequest.addLoad(allSupportedAnnotationsObject2); + expectedObjects.add(null); + + executeAndValidateTransactionLoad(mapperWithTableNameResolver, transactionLoadRequest, testData.getExpectedObjects()); + } finally { + deleteTestTable(dynamo, allSupportedAnnotationsTableName); + } + } + + private StringAttributeSubClass getStringAttributeSubClassObject() { + StringAttributeSubClass obj = new StringAttributeSubClass(); + String uuid = UUID.randomUUID().toString(); + obj.setKey(uuid); + obj.setRenamedAttribute(uuid + "1"); + obj.setStringAttribute(uuid + "2"); + return obj; + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TransactionLoadSanityTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TransactionLoadSanityTest.java new file mode 100644 index 000000000000..a8d5b14770f4 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TransactionLoadSanityTest.java @@ -0,0 +1,116 @@ +/* + * 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.mapper.dynamodb.mapper; + +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.createMultipleObjectTestDataForTransactionLoad; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.Existence; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.getStringAttributeClass; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.TransactionLoadTestData; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.TransactionLoadTestDataRequest; + +import software.amazon.awssdk.mapper.dynamodb.TransactionLoadRequest; +import software.amazon.awssdk.mapper.dynamodb.pojos.StringAttributeClass; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class TransactionLoadSanityTest extends TransactionsTestBase { + + @Test + public void testWithSingleExistingItem() { + StringAttributeClass obj = getStringAttributeClass(); + dynamoMapper.save(obj); + TransactionLoadRequest transactionLoadRequest = new TransactionLoadRequest(); + transactionLoadRequest.addLoad(obj); + + executeAndValidateTransactionLoad(transactionLoadRequest, Arrays.asList((Object) obj)); + } + + @Test + public void testWithSingleNonExistingItem() { + StringAttributeClass obj = getStringAttributeClass(); + TransactionLoadRequest transactionLoadRequest = new TransactionLoadRequest(); + transactionLoadRequest.addLoad(obj); + List expectedObjects = new ArrayList(); + expectedObjects.add(null); + + executeAndValidateTransactionLoad(transactionLoadRequest, expectedObjects); + } + + @Test + public void testWithMultipleAllExistingItemsFromSameTable() { + TransactionLoadTestDataRequest testDataRequest = new TransactionLoadTestDataRequest().withNumObjects(5) + .withExistence(Existence.ALL) + .withUseSameObjectType(true); + TransactionLoadTestData testData = createMultipleObjectTestDataForTransactionLoad(dynamoMapper, testDataRequest); + + executeAndValidateTransactionLoad(testData.getTransactionLoadRequest(), testData.getExpectedObjects()); + } + + @Test + public void testWithMultipleSomeExistingItemsFromSameTable() { + TransactionLoadTestDataRequest testDataRequest = new TransactionLoadTestDataRequest().withNumObjects(5) + .withExistence(Existence.SOME) + .withUseSameObjectType(true); + TransactionLoadTestData testData = createMultipleObjectTestDataForTransactionLoad(dynamoMapper, testDataRequest); + + executeAndValidateTransactionLoad(testData.getTransactionLoadRequest(), testData.getExpectedObjects()); + } + + @Test + public void testWithMultipleNoneExistingItemsFromSameTable() { + TransactionLoadTestDataRequest testDataRequest = new TransactionLoadTestDataRequest().withNumObjects(5) + .withExistence(Existence.NONE) + .withUseSameObjectType(true); + TransactionLoadTestData testData = createMultipleObjectTestDataForTransactionLoad(dynamoMapper, testDataRequest); + + executeAndValidateTransactionLoad(testData.getTransactionLoadRequest(), testData.getExpectedObjects()); + } + + + @Test + public void testWithMultipleAllExistingItemsFromDifferentTables() { + // Setting numObjects = 10 helps test with maximum items per transaction supported by low level SDK API + TransactionLoadTestDataRequest testDataRequest = new TransactionLoadTestDataRequest().withNumObjects(10) + .withExistence(Existence.ALL) + .withUseSameObjectType(false); + TransactionLoadTestData testData = createMultipleObjectTestDataForTransactionLoad(dynamoMapper, testDataRequest); + + executeAndValidateTransactionLoad(testData.getTransactionLoadRequest(), testData.getExpectedObjects()); + } + + @Test + public void testWithMultipleSomeExistingItemsFromDifferentTables() { + TransactionLoadTestDataRequest testDataRequest = new TransactionLoadTestDataRequest().withNumObjects(6) + .withExistence(Existence.SOME) + .withUseSameObjectType(false); + TransactionLoadTestData testData = createMultipleObjectTestDataForTransactionLoad(dynamoMapper, testDataRequest); + + executeAndValidateTransactionLoad(testData.getTransactionLoadRequest(), testData.getExpectedObjects()); + } + + @Test + public void testWithMultipleNoneExistingItemsFromDifferentTables() { + TransactionLoadTestDataRequest testDataRequest = new TransactionLoadTestDataRequest().withNumObjects(6) + .withExistence(Existence.NONE) + .withUseSameObjectType(false); + TransactionLoadTestData testData = createMultipleObjectTestDataForTransactionLoad(dynamoMapper, testDataRequest); + + executeAndValidateTransactionLoad(testData.getTransactionLoadRequest(), testData.getExpectedObjects()); + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TransactionLoadTableMapperTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TransactionLoadTableMapperTest.java new file mode 100644 index 000000000000..1db79a79f9f2 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TransactionLoadTableMapperTest.java @@ -0,0 +1,221 @@ +/* + * 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.mapper.dynamodb.mapper; + +import static software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.TableNameOverride; +import static software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.TableNameResolver; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.createMultipleObjectTestDataForTransactionLoad; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.createSdkRangeTestTable; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.deleteTestTable; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.Existence; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.TransactionLoadTestData; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.TransactionLoadTestDataRequest; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.getStringAttributeClass; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.getUniqueRangeKeyObject; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.getSchemaViolatingTestItemObject; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.getTestItemObject; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.ObjectSupplier; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMappingException; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTableMapper; +import software.amazon.awssdk.mapper.dynamodb.TransactionLoadRequest; +import software.amazon.awssdk.services.dynamodb.model.TransactionCanceledException; +import software.amazon.awssdk.mapper.dynamodb.pojos.RangeKeyClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.SchemaViolatingTestItem; +import software.amazon.awssdk.mapper.dynamodb.pojos.StringAttributeClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class TransactionLoadTableMapperTest extends TransactionsTestBase { + private static final String rangeKeySubClass1TableName = "sdk-range-d696fab0-566d-426d-b788-617f7397a9eb"; + private static final String rangeKeySubClass2TableName = "sdk-range-3789cdd0-d257-49e4-a7c2-84b4baad7a5a"; + + private static DynamoDBTableMapper tableMapper; + + @BeforeClass + public static void setUp() throws Exception { + TransactionsTestBase.setUp(); + tableMapper = dynamoMapper.newTableMapper(RangeKeyClass.class); + } + + @Test + public void testWithSingleExistingItem() { + RangeKeyClass obj = getUniqueRangeKeyObject(); + tableMapper.save(obj); + TransactionLoadRequest transactionLoadRequest = new TransactionLoadRequest(); + transactionLoadRequest.addLoad(obj); + + executeAndValidateTransactionLoad(transactionLoadRequest, Arrays.asList((Object) obj)); + } + + @Test + public void testWithSingleNonExistingItem() { + RangeKeyClass obj = getUniqueRangeKeyObject(); + TransactionLoadRequest transactionLoadRequest = new TransactionLoadRequest(); + transactionLoadRequest.addLoad(obj); + List expectedObjects = new ArrayList(); + expectedObjects.add(null); + + executeAndValidateTransactionLoad(transactionLoadRequest, expectedObjects); + } + + @Test + public void testWithSingleInvalidObjectTypeItem() { + TransactionLoadRequest transactionLoadRequest = new TransactionLoadRequest(); + transactionLoadRequest.addLoad(getStringAttributeClass()); + try { + tableMapper.transactionLoad(transactionLoadRequest); + fail("Expected DynamoDBMappingException but no exception thrown"); + } catch (DynamoDBMappingException ex) { + String expectedMessage = "Input object is of the classType: " + StringAttributeClass.class + " but tableMapper is declared with classType: " + + RangeKeyClass.class; + assertEquals(expectedMessage, ex.getMessage()); + } + } + + @Test + public void testWithMultipleSomeExistingItemsFromDifferentTables() throws InterruptedException { + final int numObjects = 10; + TableNameResolver tableNameResolver = new TableNameResolver() { + private int counter; + private boolean stillSaving = true; + + @Override + public String getTableName(Class clazz, DynamoDBMapperConfig config) { + if (!RangeKeyClass.class.equals(clazz)) { + throw new IllegalArgumentException("Input object must be of type: " + RangeKeyClass.class); + } + String tableName; + if (counter % 3 == 0) { + tableName = sdkRangeTableName; + } else if (counter % 3 == 1) { + tableName = rangeKeySubClass1TableName; + } else { + tableName = rangeKeySubClass2TableName; + } + counter++; + + // This method gets called in two flows: + // 1. From DynamoDBMapper.save for every alternate object (because we only save half of the total objects we add as input to transactionLoad) + // 2. From DynamoDBMapper.transactionLoad for every object (because we add every object as input to transactionLoad) + // So, to keep resulting table name same across save/transactionLoad for same object, we increment counter twice per call to this method while + // saving objects. + if (stillSaving) { + counter++; + } + // This means all items have been saved, so we reset counter so that names match for same object while doing transactionLoad + if (counter >= numObjects - 1) { + counter = 0; + stillSaving = false; + } + + return tableName; + } + }; + DynamoDBMapperConfig config = DynamoDBMapperConfig.builder().withTableNameResolver(tableNameResolver).build(); + DynamoDBMapper mapperWithTableNameResolver = new DynamoDBMapper(dynamo, config); + DynamoDBTableMapper tableMapperWithTableNameResolver = mapperWithTableNameResolver.newTableMapper(RangeKeyClass.class); + + ObjectSupplier objectSupplier = new ObjectSupplier() { + @Override + public Object get() { + return getUniqueRangeKeyObject(); + } + }; + + try { + createSdkRangeTestTable(dynamo, rangeKeySubClass1TableName); + createSdkRangeTestTable(dynamo, rangeKeySubClass2TableName); + TransactionLoadTestDataRequest testDataRequest = new TransactionLoadTestDataRequest().withNumObjects(numObjects) + .withExistence(Existence.SOME) + .withUseSameObjectType(true) + .withObjectSupplier(objectSupplier); + TransactionLoadTestData testData = createMultipleObjectTestDataForTransactionLoad(mapperWithTableNameResolver, testDataRequest); + + executeAndValidateTransactionLoad(tableMapperWithTableNameResolver, testData.getTransactionLoadRequest(), testData.getExpectedObjects()); + } finally { + deleteTestTable(dynamo, rangeKeySubClass1TableName); + deleteTestTable(dynamo, rangeKeySubClass2TableName); + } + } + + @Test + public void testWithMultipleSomeInvalidObjectTypeItems() { + try { + TransactionLoadRequest transactionLoadRequest = new TransactionLoadRequest(); + for (int i = 0; i < 3; i++) { + transactionLoadRequest.addLoad(getUniqueRangeKeyObject()); + } + transactionLoadRequest.addLoad(getStringAttributeClass()); + transactionLoadRequest.addLoad(getUniqueRangeKeyObject()); + transactionLoadRequest.addLoad(getTestItemObject()); + + executeAndValidateTransactionLoad(transactionLoadRequest, Collections.emptyList()); + fail("Expected DynamoDBMappingException but no exception thrown"); + } catch (DynamoDBMappingException ex) { + String expectedMessage = "Input object is of the classType: " + StringAttributeClass.class + " but tableMapper is declared with classType: " + + RangeKeyClass.class; + assertEquals(expectedMessage, ex.getMessage()); + } + } + + @Test + public void testWithMultipleItemsWhenSomeItemsCauseTransactionCancelled() { + int numObjects = 3; + TransactionLoadRequest transactionLoadRequest = new TransactionLoadRequest(); + DynamoDBMapperConfig config = DynamoDBMapperConfig.builder() + .withTableNameOverride(TableNameOverride.withTableNameReplacement(mapperSaveConfigTableName)) + .build(); + DynamoDBMapper mapperWithTableNameOverride = new DynamoDBMapper(dynamo, config); + DynamoDBTableMapper tableMapperWithTableNameOverride = + mapperWithTableNameOverride.newTableMapper(SchemaViolatingTestItem.class); + for (int i = 0; i < numObjects; i++) { + transactionLoadRequest.addLoad(getSchemaViolatingTestItemObject()); + } + + try { + executeAndValidateTransactionLoad(tableMapperWithTableNameOverride, transactionLoadRequest, Collections.emptyList()); + fail("Expected TransactionCanceledException but no exception thrown"); + } catch (TransactionCanceledException ex) { + // Expected + } + } + + @Override + protected void executeAndValidateTransactionLoad(TransactionLoadRequest transactionLoadRequest, List expectedObjects) { + executeAndValidateTransactionLoad(tableMapper, transactionLoadRequest, expectedObjects); + } + + private void executeAndValidateTransactionLoad(DynamoDBTableMapper tableMapper, + TransactionLoadRequest transactionLoadRequest, + List expectedObjects) { + List responseObjects = tableMapper.transactionLoad(transactionLoadRequest); + int numItems = expectedObjects.size(); + assertEquals(numItems, responseObjects.size()); + for (int i = 0; i < numItems; i++) { + assertEquals(expectedObjects.get(i), responseObjects.get(i)); + } + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TransactionWriteConditionExpressionTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TransactionWriteConditionExpressionTest.java new file mode 100644 index 000000000000..84d0d3d186c0 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TransactionWriteConditionExpressionTest.java @@ -0,0 +1,305 @@ +/* + * 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.mapper.dynamodb.mapper; + +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.ConditionExpressionMode; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.TransactionWriteTestData; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.TransactionWriteTestRequest; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.createMultipleObjectTestDataForTransactionWrite; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.CancellationReason; +import software.amazon.awssdk.services.dynamodb.model.TransactionCanceledException; +import software.amazon.awssdk.mapper.dynamodb.pojos.HashKeyRangeKeyClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.StringAttributeClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.TestItem; +import org.junit.Test; + +/** + * All the tests unless specified run on multiple tables with multiple operations + */ +public class TransactionWriteConditionExpressionTest extends TransactionsTestBase { + + @Test + public void testOnePutWithCondition() { + TransactionWriteTestRequest transactionWriteTestRequest = new TransactionWriteTestRequest() + .withPutConditionExpressionMode(ConditionExpressionMode.ONE) + .withUpdateConditionExpressionMode(ConditionExpressionMode.NONE) + .withDeleteConditionExpressionMode(ConditionExpressionMode.NONE); + TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(dynamoMapper, transactionWriteTestRequest); + executeAndValidateTransactionWrite(transactionWriteTestData.getTransactionWriteRequest(), + transactionWriteTestData.getExpectedObjectKeys(), + transactionWriteTestData.getExpectedObjects()); + } + + @Test + public void testOneUpdateWithCondition() { + TransactionWriteTestRequest transactionWriteTestRequest = new TransactionWriteTestRequest() + .withPutConditionExpressionMode(ConditionExpressionMode.NONE) + .withUpdateConditionExpressionMode(ConditionExpressionMode.ONE) + .withDeleteConditionExpressionMode(ConditionExpressionMode.NONE); + TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(dynamoMapper, transactionWriteTestRequest); + executeAndValidateTransactionWrite(transactionWriteTestData.getTransactionWriteRequest(), + transactionWriteTestData.getExpectedObjectKeys(), + transactionWriteTestData.getExpectedObjects()); + } + + @Test + public void testOneDeleteWithCondition() { + TransactionWriteTestRequest transactionWriteTestRequest = new TransactionWriteTestRequest() + .withPutConditionExpressionMode(ConditionExpressionMode.NONE) + .withUpdateConditionExpressionMode(ConditionExpressionMode.NONE) + .withDeleteConditionExpressionMode(ConditionExpressionMode.ONE); + TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(dynamoMapper, transactionWriteTestRequest); + executeAndValidateTransactionWrite(transactionWriteTestData.getTransactionWriteRequest(), + transactionWriteTestData.getExpectedObjectKeys(), + transactionWriteTestData.getExpectedObjects()); + } + + @Test + public void testMultiplePutsWithCondition() { + TransactionWriteTestRequest transactionWriteTestRequest = new TransactionWriteTestRequest() + .withPutConditionExpressionMode(ConditionExpressionMode.MULTIPLE) + .withUpdateConditionExpressionMode(ConditionExpressionMode.NONE) + .withDeleteConditionExpressionMode(ConditionExpressionMode.NONE); + TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(dynamoMapper, transactionWriteTestRequest); + executeAndValidateTransactionWrite(transactionWriteTestData.getTransactionWriteRequest(), + transactionWriteTestData.getExpectedObjectKeys(), + transactionWriteTestData.getExpectedObjects()); + + } + + @Test + public void testMultipleUpdatesWithCondition() { + TransactionWriteTestRequest transactionWriteTestRequest = new TransactionWriteTestRequest() + .withPutConditionExpressionMode(ConditionExpressionMode.NONE) + .withUpdateConditionExpressionMode(ConditionExpressionMode.MULTIPLE) + .withDeleteConditionExpressionMode(ConditionExpressionMode.NONE); + TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(dynamoMapper, transactionWriteTestRequest); + executeAndValidateTransactionWrite(transactionWriteTestData.getTransactionWriteRequest(), + transactionWriteTestData.getExpectedObjectKeys(), + transactionWriteTestData.getExpectedObjects()); + } + + @Test + public void testMultipleDeletesWithCondition() { + TransactionWriteTestRequest transactionWriteTestRequest = new TransactionWriteTestRequest() + .withPutConditionExpressionMode(ConditionExpressionMode.NONE) + .withUpdateConditionExpressionMode(ConditionExpressionMode.NONE) + .withDeleteConditionExpressionMode(ConditionExpressionMode.MULTIPLE); + TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(dynamoMapper, transactionWriteTestRequest); + executeAndValidateTransactionWrite(transactionWriteTestData.getTransactionWriteRequest(), + transactionWriteTestData.getExpectedObjectKeys(), + transactionWriteTestData.getExpectedObjects()); + } + + @Test + public void testMultipleOperationsWithCondition() { + TransactionWriteTestRequest transactionWriteTestRequest = new TransactionWriteTestRequest() + .withPutConditionExpressionMode(ConditionExpressionMode.MULTIPLE) + .withUpdateConditionExpressionMode(ConditionExpressionMode.MULTIPLE) + .withDeleteConditionExpressionMode(ConditionExpressionMode.MULTIPLE) + .withConditionCheckConditionExpressionMode(ConditionExpressionMode.MULTIPLE); + TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(dynamoMapper, transactionWriteTestRequest); + executeAndValidateTransactionWrite(transactionWriteTestData.getTransactionWriteRequest(), + transactionWriteTestData.getExpectedObjectKeys(), + transactionWriteTestData.getExpectedObjects()); + } + + @Test + public void testMultipleOperationsWithOneEachHavingExpressionAttributeNames() { + TransactionWriteTestRequest transactionWriteTestRequest = new TransactionWriteTestRequest() + .withPutConditionExpressionMode(ConditionExpressionMode.ONE_ATTRIBUTE_NAMES_ONLY) + .withUpdateConditionExpressionMode(ConditionExpressionMode.ONE_ATTRIBUTE_NAMES_ONLY) + .withDeleteConditionExpressionMode(ConditionExpressionMode.ONE_ATTRIBUTE_NAMES_ONLY) + .withConditionCheckConditionExpressionMode(ConditionExpressionMode.ONE_ATTRIBUTE_NAMES_ONLY); + TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(dynamoMapper, transactionWriteTestRequest); + executeAndValidateTransactionWrite(transactionWriteTestData.getTransactionWriteRequest(), + transactionWriteTestData.getExpectedObjectKeys(), + transactionWriteTestData.getExpectedObjects()); + } + + @Test + public void testMultipleOperationsWithMultipleOfEachHavingExpressionAttributeNames() { + TransactionWriteTestRequest transactionWriteTestRequest = new TransactionWriteTestRequest() + .withPutConditionExpressionMode(ConditionExpressionMode.MULTIPLE_ATTRIBUTE_NAMES_ONLY) + .withUpdateConditionExpressionMode(ConditionExpressionMode.MULTIPLE_ATTRIBUTE_NAMES_ONLY) + .withDeleteConditionExpressionMode(ConditionExpressionMode.MULTIPLE_ATTRIBUTE_NAMES_ONLY) + .withConditionCheckConditionExpressionMode(ConditionExpressionMode.MULTIPLE_ATTRIBUTE_NAMES_ONLY); + TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(dynamoMapper, transactionWriteTestRequest); + executeAndValidateTransactionWrite(transactionWriteTestData.getTransactionWriteRequest(), + transactionWriteTestData.getExpectedObjectKeys(), + transactionWriteTestData.getExpectedObjects()); + } + + @Test + public void testMultipleOperationsWithOneOfEachHavingExpressionAttributeNamesValues() { + TransactionWriteTestRequest transactionWriteTestRequest = new TransactionWriteTestRequest() + .withPutConditionExpressionMode(ConditionExpressionMode.ONE_ATTRIBUTE_NAMES_VALUES) + .withUpdateConditionExpressionMode(ConditionExpressionMode.ONE_ATTRIBUTE_NAMES_VALUES) + .withDeleteConditionExpressionMode(ConditionExpressionMode.ONE_ATTRIBUTE_NAMES_VALUES) + .withConditionCheckConditionExpressionMode(ConditionExpressionMode.ONE_ATTRIBUTE_NAMES_VALUES); + TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(dynamoMapper, transactionWriteTestRequest); + executeAndValidateTransactionWrite(transactionWriteTestData.getTransactionWriteRequest(), + transactionWriteTestData.getExpectedObjectKeys(), + transactionWriteTestData.getExpectedObjects()); + } + + @Test + public void testMultipleOperationsWithOneEachHavingExpressionAttributeValues() { + TransactionWriteTestRequest transactionWriteTestRequest = new TransactionWriteTestRequest() + .withPutConditionExpressionMode(ConditionExpressionMode.ONE_ATTRIBUTE_VALUES_ONLY) + .withUpdateConditionExpressionMode(ConditionExpressionMode.ONE_ATTRIBUTE_VALUES_ONLY) + .withDeleteConditionExpressionMode(ConditionExpressionMode.ONE_ATTRIBUTE_VALUES_ONLY) + .withConditionCheckConditionExpressionMode(ConditionExpressionMode.ONE_ATTRIBUTE_VALUES_ONLY); + TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(dynamoMapper, transactionWriteTestRequest); + executeAndValidateTransactionWrite(transactionWriteTestData.getTransactionWriteRequest(), + transactionWriteTestData.getExpectedObjectKeys(), + transactionWriteTestData.getExpectedObjects()); + } + + @Test + public void testMultipleOperationsWithMultipleOfEachHavingExpressionAttributeValues() { + TransactionWriteTestRequest transactionWriteTestRequest = new TransactionWriteTestRequest() + .withPutConditionExpressionMode(ConditionExpressionMode.MULTIPLE_ATTRIBUTE_VALUES_ONLY) + .withUpdateConditionExpressionMode(ConditionExpressionMode.MULTIPLE_ATTRIBUTE_VALUES_ONLY) + .withDeleteConditionExpressionMode(ConditionExpressionMode.MULTIPLE_ATTRIBUTE_VALUES_ONLY) + .withConditionCheckConditionExpressionMode(ConditionExpressionMode.MULTIPLE_ATTRIBUTE_VALUES_ONLY); + TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(dynamoMapper, transactionWriteTestRequest); + executeAndValidateTransactionWrite(transactionWriteTestData.getTransactionWriteRequest(), + transactionWriteTestData.getExpectedObjectKeys(), + transactionWriteTestData.getExpectedObjects()); + } + + @Test + public void testMultipleOperationsWithOneEachHavingSimpleConditionExpression() { + TransactionWriteTestRequest transactionWriteTestRequest = new TransactionWriteTestRequest() + .withPutConditionExpressionMode(ConditionExpressionMode.ONE) + .withUpdateConditionExpressionMode(ConditionExpressionMode.ONE) + .withDeleteConditionExpressionMode(ConditionExpressionMode.ONE); + TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(dynamoMapper, transactionWriteTestRequest); + executeAndValidateTransactionWrite(transactionWriteTestData.getTransactionWriteRequest(), + transactionWriteTestData.getExpectedObjectKeys(), + transactionWriteTestData.getExpectedObjects()); + } + + @Test + public void testMultipleOperationsWithMultipleOfEachHavingExpressionAttributeNamesAndValues() { + TransactionWriteTestRequest transactionWriteTestRequest = new TransactionWriteTestRequest() + .withPutConditionExpressionMode(ConditionExpressionMode.MULTIPLE_ATTRIBUTE_NAMES_VALUES) + .withUpdateConditionExpressionMode(ConditionExpressionMode.MULTIPLE_ATTRIBUTE_NAMES_VALUES) + .withDeleteConditionExpressionMode(ConditionExpressionMode.MULTIPLE_ATTRIBUTE_NAMES_VALUES) + .withConditionCheckConditionExpressionMode(ConditionExpressionMode.MULTIPLE_ATTRIBUTE_NAMES_VALUES); + TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(dynamoMapper, transactionWriteTestRequest); + executeAndValidateTransactionWrite(transactionWriteTestData.getTransactionWriteRequest(), + transactionWriteTestData.getExpectedObjectKeys(), + transactionWriteTestData.getExpectedObjects()); + } + + @Test + public void testMultipleOperationsWithOneOfEachHavingInvalidExpression() { + TransactionWriteTestRequest transactionWriteTestRequest = new TransactionWriteTestRequest() + .withPutConditionExpressionMode(ConditionExpressionMode.ONE_INVALID_RETURN_VALUES_SET) + .withUpdateConditionExpressionMode(ConditionExpressionMode.ONE_INVALID_RETURN_VALUES_SET) + .withDeleteConditionExpressionMode(ConditionExpressionMode.ONE_INVALID_RETURN_VALUES_SET) + .withConditionCheckConditionExpressionMode(ConditionExpressionMode.ONE_INVALID_RETURN_VALUES_SET) + .withIsMultiTable(true); + TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(dynamoMapper, transactionWriteTestRequest); + try { + executeAndValidateTransactionWrite(transactionWriteTestData.getTransactionWriteRequest(), + transactionWriteTestData.getExpectedObjectKeys(), + transactionWriteTestData.getExpectedObjects()); + fail("Expected TransactionCanceledException but no exception thrown"); + } catch (TransactionCanceledException tce) { + // Expected + } + } + + @Test + public void testMultipleOperationsWithMultipleOfEachHavingInvalidExpression() { + /** + * TODO: This is currently failing on multi-table mode due to an issue with the low level API. + * Please flip this to multi-table mode once this issue is resolved. + */ + TransactionWriteTestRequest transactionWriteTestRequest = new TransactionWriteTestRequest() + .withPutConditionExpressionMode(ConditionExpressionMode.MULTIPLE_INVALID_RETURN_VALUES_SET) + .withUpdateConditionExpressionMode(ConditionExpressionMode.MULTIPLE_INVALID_RETURN_VALUES_SET) + .withDeleteConditionExpressionMode(ConditionExpressionMode.MULTIPLE_INVALID_RETURN_VALUES_SET) + .withConditionCheckConditionExpressionMode(ConditionExpressionMode.MULTIPLE_INVALID_RETURN_VALUES_SET) + .withIsMultiTable(false); + + TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(dynamoMapper, transactionWriteTestRequest); + try { + executeAndValidateTransactionWrite(transactionWriteTestData.getTransactionWriteRequest(), + transactionWriteTestData.getExpectedObjectKeys(), + transactionWriteTestData.getExpectedObjects()); + fail("Expected TransactionCanceledException but no exception thrown"); + } catch (TransactionCanceledException tce) { + // Expected + } + } + + @Test + public void testMultipleOperationsWithOneOfEachHavingReservedWordExpression() { + TransactionWriteTestRequest transactionWriteTestRequest = new TransactionWriteTestRequest() + .withPutConditionExpressionMode(ConditionExpressionMode.ONE_RESERVED_WORD) + .withUpdateConditionExpressionMode(ConditionExpressionMode.ONE_RESERVED_WORD) + .withDeleteConditionExpressionMode(ConditionExpressionMode.ONE_RESERVED_WORD) + .withConditionCheckConditionExpressionMode(ConditionExpressionMode.ONE_RESERVED_WORD); + TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(dynamoMapper, transactionWriteTestRequest); + try { + executeAndValidateTransactionWrite(transactionWriteTestData.getTransactionWriteRequest(), + transactionWriteTestData.getExpectedObjectKeys(), + transactionWriteTestData.getExpectedObjects()); + fail("Expected AwsServiceException but no exception thrown"); + } catch (AwsServiceException ase) { + assertEquals("ValidationException", ase.awsErrorDetails().errorCode()); + } + } + + private static Map generateAttributeItemFromObject(Object obj) { + Map expectedResponseValueMap = new HashMap(); + if (obj.getClass().equals(StringAttributeClass.class)) { + StringAttributeClass stringAttributeClassObject = (StringAttributeClass) obj; + expectedResponseValueMap.put("originalName", AttributeValue.builder().s(stringAttributeClassObject.getRenamedAttribute()).build()); + expectedResponseValueMap.put("stringAttribute", AttributeValue.builder().s(stringAttributeClassObject.getStringAttribute()).build()); + expectedResponseValueMap.put("key", AttributeValue.builder().s(stringAttributeClassObject.getKey()).build()); + } else if (obj.getClass().equals(TestItem.class)) { + TestItem testItemObject = (TestItem) obj; + expectedResponseValueMap.put("hashKey", AttributeValue.builder().s(testItemObject.getHashKey()).build()); + expectedResponseValueMap.put("rangeKey", AttributeValue.builder().n(testItemObject.getRangeKey().toString()).build()); + expectedResponseValueMap.put("stringAttribute", AttributeValue.builder().s(testItemObject.getStringAttribute()).build()); + expectedResponseValueMap.put("nonKeyAttribute", AttributeValue.builder().s(testItemObject.getNonKeyAttribute()).build()); + expectedResponseValueMap.put("stringSetAttribute", AttributeValue.builder().ss(testItemObject.getStringSetAttribute()).build()); + } else if (obj.getClass().equals(HashKeyRangeKeyClass.class)) { + HashKeyRangeKeyClass hashKeyRangeKeyClassObject = (HashKeyRangeKeyClass) obj; + expectedResponseValueMap.put("hashKey", AttributeValue.builder().n(Long.toString(hashKeyRangeKeyClassObject.getHashKey())).build()); + expectedResponseValueMap.put("rangeKey", AttributeValue.builder().n(Double.toString(hashKeyRangeKeyClassObject.getRangeKey())).build()); + expectedResponseValueMap.put("stringAttribute", AttributeValue.builder().s(hashKeyRangeKeyClassObject.getStringAttribute()).build()); + } else { + throw new IllegalArgumentException("Unsupported class passed in: " + obj.getClass()); + } + return expectedResponseValueMap; + } + +} \ No newline at end of file diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TransactionWriteMiscellaneousTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TransactionWriteMiscellaneousTest.java new file mode 100644 index 000000000000..9bd9625a75c6 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TransactionWriteMiscellaneousTest.java @@ -0,0 +1,501 @@ +/* + * 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.mapper.dynamodb.mapper; + +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.ConditionExpressionMode; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.TransactionWriteTestData; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.createAllSupportedAnnotationsTable; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.createMultipleObjectTestDataForTransactionWrite; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.createSdkStringRangeTable; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.deleteTestTable; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.extractAllObjectsFromTransactionWriteOperations; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.extractExpectedResponseObjects; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.extractObjectKeysForValidResponseSendingOperations; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.getAllSupportedAnnotationsObject; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.getAllSupportedDataTypesObject; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.getSlimAttributeNamesClass; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.updateTransactionWriteOperationsWithNewObject; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.updateTransactionWriteOperationsWithNewObjectAndExpression; +import static software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.TableNameResolver; +import static software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.builder; +import static software.amazon.awssdk.mapper.dynamodb.TransactionWriteRequest.TransactionWriteOperation; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.mapper.dynamodb.TestObjectCreator; +import software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.TransactionWriteTestRequest; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMappingException; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBQueryExpression; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTransactionWriteExpression; +import software.amazon.awssdk.mapper.dynamodb.PaginatedQueryList; +import software.amazon.awssdk.mapper.dynamodb.TransactionWriteRequest; +import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest; +import software.amazon.awssdk.services.dynamodb.model.GlobalSecondaryIndex; +import software.amazon.awssdk.services.dynamodb.model.IdempotentParameterMismatchException; +import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement; +import software.amazon.awssdk.services.dynamodb.model.KeyType; +import software.amazon.awssdk.services.dynamodb.model.Projection; +import software.amazon.awssdk.services.dynamodb.model.ProjectionType; +import software.amazon.awssdk.services.dynamodb.model.ResourceNotFoundException; +import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType; +import software.amazon.awssdk.mapper.dynamodb.pojos.AllSupportedAnnotationsClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.AllSupportedDataTypesClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.HashKeyRangeKeyClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.PrimaryKeysNotAutogenerated_IndexKeysAutogenerated; +import software.amazon.awssdk.mapper.dynamodb.pojos.SlimAttributeNamesClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.StringAttributeClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.TestItem; +import org.junit.Test; + +public class TransactionWriteMiscellaneousTest extends TransactionsTestBase { + private static final String tableNameSuffix = UUID.randomUUID().toString(); + private static final String nonExistingTableName = "nonExistingTable-" + tableNameSuffix; + private static final String sdkStringRangeTableNameWithGsi = "sdk-string-range-" + tableNameSuffix; + private static final String SDK_STRING_RANGE_TABLE_GSI_NAME = "gsi-with-autogenerated-keys"; + + // TODO: Refactor tableNameResolvers to re-use code between multiple such instances in transactionLoad/transactionWrite integration tests + private static final TableNameResolver tableNameResolverForMissingTableTests = new TableNameResolver() { + @Override + public String getTableName(Class clazz, DynamoDBMapperConfig config) { + if (StringAttributeClass.class.equals(clazz)) { + return sdkUtilTableName; + } + if (TestItem.class.equals(clazz)) { + return mapperSaveConfigTableName; + } + if (HashKeyRangeKeyClass.class.equals(clazz)) { + return sdkHashRangeTestTableName; + } + if (NoSuchTableClass.class.equals(clazz)) { + return nonExistingTableName; + } + throw new RuntimeException("Unable to resolve table name for input object type: " + clazz); + } + }; + + private static final TableNameResolver tableNameResolverForGsiTest = new TableNameResolver() { + @Override + public String getTableName(Class clazz, DynamoDBMapperConfig config) { + if (StringAttributeClass.class.equals(clazz)) { + return sdkUtilTableName; + } + if (TestItem.class.equals(clazz)) { + return mapperSaveConfigTableName; + } + if (PrimaryKeysNotAutogenerated_IndexKeysAutogenerated.class.equals(clazz)) { + return sdkStringRangeTableNameWithGsi; + } + if (HashKeyRangeKeyClass.class.equals(clazz)) { + return sdkHashRangeTestTableName; + } + throw new RuntimeException("Unable to resolve table name for input object type: " + clazz); + } + }; + + @Test + public void testWithTableMissingForOneItem() { + DynamoDBMapperConfig config = DynamoDBMapperConfig.builder().withTableNameResolver(tableNameResolverForMissingTableTests) + .build(); + DynamoDBMapper mapperWithTableNameResolver = new DynamoDBMapper(dynamo, config); + TransactionWriteTestRequest transactionWriteTestRequest = new TransactionWriteTestRequest() + .withPutConditionExpressionMode(ConditionExpressionMode.NONE) + .withUpdateConditionExpressionMode(ConditionExpressionMode.NONE) + .withDeleteConditionExpressionMode(ConditionExpressionMode.NONE) + .withIsMultiTable(true); + + TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(mapperWithTableNameResolver, transactionWriteTestRequest); + + NoSuchTableClass objectToBePut = new NoSuchTableClass(); + objectToBePut.setKey("somekey"); + + List transactionWriteOperations = transactionWriteTestData.getTransactionWriteRequest().getTransactionWriteOperations(); + + updateTransactionWriteOperationsWithNewObject(transactionWriteOperations, + objectToBePut, + 1); + + try { + executeAndValidateTransactionWrite(mapperWithTableNameResolver, + transactionWriteTestData.getTransactionWriteRequest(), + transactionWriteTestData.getExpectedObjectKeys(), + transactionWriteTestData.getExpectedObjects()); + fail("Expected ResourceNotFoundException but no exception thrown"); + } catch (ResourceNotFoundException e) { + // Pass + } + } + + @Test + public void testWithTableMissingForMultipleItems() { + DynamoDBMapperConfig config = DynamoDBMapperConfig.builder().withTableNameResolver(tableNameResolverForMissingTableTests) + .build(); + DynamoDBMapper mapperWithTableNameResolver = new DynamoDBMapper(dynamo, config); + TransactionWriteTestRequest transactionWriteTestRequest = new TransactionWriteTestRequest() + .withPutConditionExpressionMode(ConditionExpressionMode.NONE) + .withUpdateConditionExpressionMode(ConditionExpressionMode.NONE) + .withDeleteConditionExpressionMode(ConditionExpressionMode.NONE) + .withIsMultiTable(true); + + TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(mapperWithTableNameResolver, transactionWriteTestRequest); + + NoSuchTableClass objectToBePut = new NoSuchTableClass(); + objectToBePut.setKey("somekey"); + NoSuchTableClass objectToBeUpdated = new NoSuchTableClass(); + objectToBeUpdated.setKey("somekey"); + objectToBeUpdated.setOtherAttribute("someAttribute"); + NoSuchTableClass objectToBeConditionChecked = new NoSuchTableClass(); + objectToBeConditionChecked.setKey("somekey"); + NoSuchTableClass objectToBeDeleted = new NoSuchTableClass(); + objectToBeDeleted.setKey("somekey"); + + List transactionWriteOperations = transactionWriteTestData.getTransactionWriteRequest().getTransactionWriteOperations(); + + updateTransactionWriteOperationsWithNewObject(transactionWriteOperations, objectToBePut, 0); + updateTransactionWriteOperationsWithNewObject(transactionWriteOperations, objectToBeUpdated, 2); + updateTransactionWriteOperationsWithNewObject(transactionWriteOperations, objectToBeConditionChecked, 4); + updateTransactionWriteOperationsWithNewObject(transactionWriteOperations, objectToBeDeleted, 5); + + try { + executeAndValidateTransactionWrite(mapperWithTableNameResolver, + transactionWriteTestData.getTransactionWriteRequest(), + transactionWriteTestData.getExpectedObjectKeys(), + transactionWriteTestData.getExpectedObjects()); + fail("Expected ResourceNotFoundException but no exception thrown"); + } catch (ResourceNotFoundException e) { + // Pass + } + } + + @Test + public void testWithSameIdempotencyToken() { + String idempotencyToken = UUID.randomUUID().toString(); + + TransactionWriteTestRequest transactionWriteTestRequest = new TransactionWriteTestRequest() + .withPutConditionExpressionMode(ConditionExpressionMode.NONE) + .withUpdateConditionExpressionMode(ConditionExpressionMode.NONE) + .withDeleteConditionExpressionMode(ConditionExpressionMode.NONE) + .withIsMultiTable(true); + TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(dynamoMapper, transactionWriteTestRequest); + transactionWriteTestData.getTransactionWriteRequest().withIdempotencyToken(idempotencyToken); + for (int i = 0; i < 10; i++) { + executeAndValidateTransactionWrite(transactionWriteTestData.getTransactionWriteRequest(), + transactionWriteTestData.getExpectedObjectKeys(), + transactionWriteTestData.getExpectedObjects()); + } + } + + @Test + public void testWithSameIdempotencyTokenOnDifferentRequest() { + String idempotencyToken = UUID.randomUUID().toString(); + TransactionWriteTestRequest transactionWriteTestRequest = new TransactionWriteTestRequest() + .withPutConditionExpressionMode(ConditionExpressionMode.NONE) + .withUpdateConditionExpressionMode(ConditionExpressionMode.NONE) + .withDeleteConditionExpressionMode(ConditionExpressionMode.NONE) + .withIsMultiTable(true); + TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(dynamoMapper, transactionWriteTestRequest); + SlimAttributeNamesClass objectToBeUpdated1 = getSlimAttributeNamesClass(); + objectToBeUpdated1.setC("UpdatedString1"); + List transactionWriteOperations = transactionWriteTestData.getTransactionWriteRequest().getTransactionWriteOperations(); + updateTransactionWriteOperationsWithNewObject(transactionWriteOperations, objectToBeUpdated1, 3); + List allExistingObjects = extractAllObjectsFromTransactionWriteOperations(transactionWriteOperations); + List responseObjectKeys = extractObjectKeysForValidResponseSendingOperations(allExistingObjects); + List responseObjects = extractExpectedResponseObjects(allExistingObjects); + transactionWriteTestData.getTransactionWriteRequest().withIdempotencyToken(idempotencyToken); + executeAndValidateTransactionWrite(transactionWriteTestData.getTransactionWriteRequest(), + responseObjectKeys, + responseObjects); + objectToBeUpdated1 = getSlimAttributeNamesClass(); + objectToBeUpdated1.setC("UpdatedString11"); + transactionWriteOperations = transactionWriteTestData.getTransactionWriteRequest().getTransactionWriteOperations(); + updateTransactionWriteOperationsWithNewObject(transactionWriteOperations, objectToBeUpdated1, 3); + try { + executeAndValidateTransactionWrite(transactionWriteTestData.getTransactionWriteRequest(), + responseObjectKeys, + responseObjects); + fail("Expected IdempotentParameterMismatchException but no exception thrown"); + } catch (IdempotentParameterMismatchException ipme) { + // Pass + } + } + + @Test + public void testWithDifferentIdempotencyTokenOnSameRequest() { + String baseIdempotencyToken = UUID.randomUUID().toString(); + String idempotencyToken1 = baseIdempotencyToken.substring(0, 35) + "1"; + String idempotencyToken2 = baseIdempotencyToken.substring(0, 35) + "2"; + TransactionWriteTestRequest transactionWriteTestRequest = new TransactionWriteTestRequest() + .withPutConditionExpressionMode(ConditionExpressionMode.NONE) + .withUpdateConditionExpressionMode(ConditionExpressionMode.NONE) + .withDeleteConditionExpressionMode(ConditionExpressionMode.NONE) + .withIsMultiTable(true); + TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(dynamoMapper, transactionWriteTestRequest); + TransactionWriteRequest transactionWriteRequest = transactionWriteTestData.getTransactionWriteRequest(); + transactionWriteTestData.getTransactionWriteRequest().withIdempotencyToken(idempotencyToken1); + executeAndValidateTransactionWrite(transactionWriteRequest, + transactionWriteTestData.getExpectedObjectKeys(), + transactionWriteTestData.getExpectedObjects()); + transactionWriteTestData.getTransactionWriteRequest().withIdempotencyToken(idempotencyToken2); + executeAndValidateTransactionWrite(transactionWriteRequest, + transactionWriteTestData.getExpectedObjectKeys(), + transactionWriteTestData.getExpectedObjects()); + + } + + @Test + public void testWithItemsHavingAllSupportedDataTypes() throws InterruptedException { + final String allSupportedTypesTableName = "all-supported-types-" + tableNameSuffix; + // TODO: Refactor tableNameResolver to re-use code between multiple such instances in transactionLoad/transactionWrite integration tests + TableNameResolver tableNameResolver = new TableNameResolver() { + @Override + public String getTableName(Class clazz, DynamoDBMapperConfig config) { + if (StringAttributeClass.class.equals(clazz)) { + return sdkUtilTableName; + } + if (TestItem.class.equals(clazz)) { + return mapperSaveConfigTableName; + } + if (HashKeyRangeKeyClass.class.equals(clazz)) { + return sdkHashRangeTestTableName; + } + if (AllSupportedDataTypesClass.class.equals(clazz)) { + return allSupportedTypesTableName; + } + throw new RuntimeException("Unable to resolve table name for input object type: " + clazz); + } + }; + DynamoDBMapperConfig config = builder().withTableNameResolver(tableNameResolver) + .build(); + DynamoDBMapper mapperWithTableNameResolver = new DynamoDBMapper(dynamo, config); + try { + createSdkStringRangeTable(dynamo, allSupportedTypesTableName); + + TransactionWriteTestRequest transactionWriteTestRequest = new TransactionWriteTestRequest() + .withPutConditionExpressionMode(ConditionExpressionMode.NONE) + .withUpdateConditionExpressionMode(ConditionExpressionMode.NONE) + .withDeleteConditionExpressionMode(ConditionExpressionMode.NONE) + .withIsMultiTable(true); + TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(dynamoMapper, transactionWriteTestRequest); + + AllSupportedDataTypesClass objectToBePut = getAllSupportedDataTypesObject(); + AllSupportedDataTypesClass objectToBeUpdated = getAllSupportedDataTypesObject(); + AllSupportedDataTypesClass objectToBeDeleted = getAllSupportedDataTypesObject(); + saveObjects(mapperWithTableNameResolver, objectToBePut, objectToBeUpdated, objectToBeDeleted); + List transactionWriteOperations = transactionWriteTestData.getTransactionWriteRequest().getTransactionWriteOperations(); + updateTransactionWriteOperationsWithNewObject(transactionWriteOperations, objectToBePut, 0); + updateTransactionWriteOperationsWithNewObject(transactionWriteOperations, objectToBeUpdated, 2); + updateTransactionWriteOperationsWithNewObject(transactionWriteOperations, objectToBeDeleted, 4); + List allExistingObjects = extractAllObjectsFromTransactionWriteOperations(transactionWriteOperations); + List responseObjectKeys = extractObjectKeysForValidResponseSendingOperations(allExistingObjects); + List responseObjects = extractExpectedResponseObjects(allExistingObjects); + executeAndValidateTransactionWrite(mapperWithTableNameResolver, + transactionWriteTestData.getTransactionWriteRequest(), + responseObjectKeys, + responseObjects); + } finally { + deleteTestTable(dynamo, allSupportedTypesTableName); + } + } + + @Test + public void testWithItemsHavingAllSupportedAnnotations() throws InterruptedException { + final String allSupportedAnnotationsTableName = "all-supported-annotations-" + tableNameSuffix; + // TODO: Refactor tableNameResolver to re-use code between multiple such instances in transactionLoad/transactionWrite integration tests + TableNameResolver tableNameResolver = new TableNameResolver() { + @Override + public String getTableName(Class clazz, DynamoDBMapperConfig config) { + if (StringAttributeClass.class.equals(clazz)) { + return sdkUtilTableName; + } + if (TestItem.class.equals(clazz)) { + return mapperSaveConfigTableName; + } + if (HashKeyRangeKeyClass.class.equals(clazz)) { + return sdkHashRangeTestTableName; + } + if (AllSupportedAnnotationsClass.class.equals(clazz)) { + return allSupportedAnnotationsTableName; + } + throw new RuntimeException("Unable to resolve table name for input object type: " + clazz); + } + }; + DynamoDBMapperConfig config = builder().withTableNameResolver(tableNameResolver) + .build(); + DynamoDBMapper mapperWithTableNameResolver = new DynamoDBMapper(dynamo, config); + try { + createAllSupportedAnnotationsTable(dynamo, allSupportedAnnotationsTableName); + TransactionWriteTestRequest transactionWriteTestRequest = new TransactionWriteTestRequest() + .withPutConditionExpressionMode(ConditionExpressionMode.NONE) + .withUpdateConditionExpressionMode(ConditionExpressionMode.NONE) + .withDeleteConditionExpressionMode(ConditionExpressionMode.NONE) + .withIsMultiTable(true); + TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(dynamoMapper, transactionWriteTestRequest); + AllSupportedAnnotationsClass objectToBePut = getAllSupportedAnnotationsObject(); + AllSupportedAnnotationsClass objectToBeUpdated = getAllSupportedAnnotationsObject(); + saveObjects(mapperWithTableNameResolver, objectToBePut, objectToBeUpdated); + // Update the IgnoredAttribute value to assert against them later + objectToBePut.setIgnoredAttribute("IgnoredPutValue"); + objectToBeUpdated.setIgnoredAttribute("IgnoredUpdateValue"); + List transactionWriteOperations = transactionWriteTestData.getTransactionWriteRequest().getTransactionWriteOperations(); + updateTransactionWriteOperationsWithNewObject(transactionWriteOperations, objectToBePut, 0); + updateTransactionWriteOperationsWithNewObject(transactionWriteOperations, objectToBeUpdated, 3); + List allExistingObjects = extractAllObjectsFromTransactionWriteOperations(transactionWriteOperations); + List responseObjectKeys = extractObjectKeysForValidResponseSendingOperations(allExistingObjects); + List responseObjects = extractExpectedResponseObjects(allExistingObjects); + // Ignored attribute value shouldn't change + objectToBePut.setIgnoredAttribute(AllSupportedAnnotationsClass.IGNORED_ATTRIBUTE_VALUE); + objectToBeUpdated.setIgnoredAttribute(AllSupportedAnnotationsClass.IGNORED_ATTRIBUTE_VALUE); + executeAndValidateTransactionWrite(mapperWithTableNameResolver, + transactionWriteTestData.getTransactionWriteRequest(), + responseObjectKeys, + responseObjects); + } finally { + deleteTestTable(dynamo, allSupportedAnnotationsTableName); + } + } + + @Test + public void testSanityOfGsiWhenUsingTransactions() throws InterruptedException { + try { + createGsiTestTable(dynamo, sdkStringRangeTableNameWithGsi); + DynamoDBMapperConfig config = DynamoDBMapperConfig.builder().withTableNameResolver(tableNameResolverForGsiTest) + .build(); + DynamoDBMapper mapperWithTableNameResolver = new DynamoDBMapper(dynamo, config); + TransactionWriteTestRequest transactionWriteTestRequest = new TransactionWriteTestRequest() + .withPutConditionExpressionMode(ConditionExpressionMode.NONE) + .withUpdateConditionExpressionMode(ConditionExpressionMode.NONE) + .withDeleteConditionExpressionMode(ConditionExpressionMode.NONE) + .withIsMultiTable(true); + TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(mapperWithTableNameResolver, transactionWriteTestRequest); + PrimaryKeysNotAutogenerated_IndexKeysAutogenerated objectToBePut = new PrimaryKeysNotAutogenerated_IndexKeysAutogenerated(); + String baseKey = UUID.randomUUID().toString(); + String hashKey = baseKey + "hash"; + String rangeKey = baseKey + "range"; + objectToBePut.setKey(hashKey); + objectToBePut.setRangeKey(rangeKey); + List transactionWriteOperations = transactionWriteTestData.getTransactionWriteRequest().getTransactionWriteOperations(); + updateTransactionWriteOperationsWithNewObject(transactionWriteOperations, objectToBePut, 0); + List allExistingObjects = extractAllObjectsFromTransactionWriteOperations(transactionWriteOperations); + List responseObjectKeys = extractObjectKeysForValidResponseSendingOperations(allExistingObjects); + List responseObjects = extractExpectedResponseObjects(allExistingObjects); + executeAndValidateTransactionWrite(mapperWithTableNameResolver, + transactionWriteTestData.getTransactionWriteRequest(), + responseObjectKeys, + responseObjects); + // Verify GSI based query returns the actual item written transactionally + final DynamoDBQueryExpression queryExpression = + new DynamoDBQueryExpression(); + queryExpression.setHashKeyValues(objectToBePut); + queryExpression.setIndexName(SDK_STRING_RANGE_TABLE_GSI_NAME); + queryExpression.setConsistentRead(false); // GSI supports only eventually consistent reads + + // Wait for up to one minute for a GSI query to return expected item because GSI query is eventually consistent. + long endTime = System.currentTimeMillis() + 60 * 1000; + PaginatedQueryList results = null; + while (System.currentTimeMillis() < endTime) { + results = mapperWithTableNameResolver.query(PrimaryKeysNotAutogenerated_IndexKeysAutogenerated.class, queryExpression); + if (results.size() == 1) { + break; + } + Thread.sleep(1000); + } + assertEquals(1, results.size()); + assertEquals(objectToBePut, results.get(0)); + } finally { + deleteTestTable(dynamo, sdkStringRangeTableNameWithGsi); + } + } + + @Test + public void testUpdatesThatFailWithTokenCollision() { + TransactionWriteTestRequest transactionWriteTestRequest = new TransactionWriteTestRequest() + .withPutConditionExpressionMode(ConditionExpressionMode.NONE) + .withUpdateConditionExpressionMode(ConditionExpressionMode.NONE) + .withDeleteConditionExpressionMode(ConditionExpressionMode.NONE) + .withIsMultiTable(true); + + TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(dynamoMapper, transactionWriteTestRequest); + SlimAttributeNamesClass objectToBeUpdated1 = getSlimAttributeNamesClass(); + SlimAttributeNamesClass objectToBeUpdated2 = getSlimAttributeNamesClass(); + SlimAttributeNamesClass objectToBeUpdated3 = getSlimAttributeNamesClass(); + objectToBeUpdated1.setC("UpdatedString1"); + List transactionWriteOperations = transactionWriteTestData.getTransactionWriteRequest().getTransactionWriteOperations(); + DynamoDBTransactionWriteExpression writeExpression1 = new DynamoDBTransactionWriteExpression(); + writeExpression1.withConditionExpression("(#14b10 IN (:attr1, :attr2)) AND (#14b102850 IN (:attr1, :attr2))"); + Map attributeNameMap = new HashMap(); + attributeNameMap.put("#14b10", "C"); + attributeNameMap.put("#14b102850", "C"); + Map attributeValueMap = new HashMap(); + attributeValueMap.put(":attr1", AttributeValue.builder().s(objectToBeUpdated1.getC()).build()); + attributeValueMap.put(":attr2", AttributeValue.builder().s("randomString").build()); + writeExpression1.withExpressionAttributeValues(attributeValueMap); + writeExpression1.withExpressionAttributeNames(attributeNameMap); + updateTransactionWriteOperationsWithNewObjectAndExpression(transactionWriteOperations, objectToBeUpdated1, writeExpression1, 3); + updateTransactionWriteOperationsWithNewObject(transactionWriteOperations, objectToBeUpdated2, 4); + updateTransactionWriteOperationsWithNewObject(transactionWriteOperations, objectToBeUpdated3, 5); + try { + executeAndValidateTransactionWrite(dynamoMapper, + transactionWriteTestData.getTransactionWriteRequest(), + transactionWriteTestData.getExpectedObjectKeys(), + transactionWriteTestData.getExpectedObjects()); + fail("Expected DynamoDBMappingException but no exception thrown"); + } catch (DynamoDBMappingException dme) { + assertEquals("Failed to process update operation inside transactionWrite request due to conflict with expressionAttributeName " + + "or expressionAttributeValue token name: 14b102850. Please replace this token name with a different token name.", + dme.getMessage()); + } + } + + private static void createGsiTestTable(DynamoDbClient dynamo, String tableName) throws InterruptedException { + CreateTableRequest createTableRequest = + CreateTableRequest.builder() + .tableName(tableName) + .keySchema(KeySchemaElement.builder().attributeName("key").keyType(KeyType.HASH).build(), + KeySchemaElement.builder().attributeName("rangeKey").keyType(KeyType.RANGE).build()) + .globalSecondaryIndexes( + GlobalSecondaryIndex.builder() + .indexName(SDK_STRING_RANGE_TABLE_GSI_NAME) + .keySchema( + KeySchemaElement.builder().attributeName("gis-hash-key").keyType(KeyType.HASH).build(), + KeySchemaElement.builder().attributeName("gis-range-key").keyType(KeyType.RANGE).build()) + .projection( + Projection.builder().projectionType(ProjectionType.ALL).build()) + .provisionedThroughput(TestObjectCreator.DEFAULT_PROVISIONED_THROUGHPUT) + .build()) + .attributeDefinitions( + AttributeDefinition.builder().attributeName("key").attributeType(ScalarAttributeType.S).build(), + AttributeDefinition.builder().attributeName("rangeKey").attributeType(ScalarAttributeType.S).build(), + AttributeDefinition.builder().attributeName("gis-hash-key").attributeType(ScalarAttributeType.S).build(), + AttributeDefinition.builder().attributeName("gis-range-key").attributeType(ScalarAttributeType.S).build()) + .provisionedThroughput(TestObjectCreator.DEFAULT_PROVISIONED_THROUGHPUT) + .build(); + try { + dynamo.createTable(createTableRequest); + dynamo.waiter().waitUntilTableExists(b -> b.tableName(tableName)); + } catch (software.amazon.awssdk.services.dynamodb.model.ResourceInUseException e) { + // Table already exists. + } + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TransactionWriteMixedTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TransactionWriteMixedTest.java new file mode 100644 index 000000000000..6a6e67374c78 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TransactionWriteMixedTest.java @@ -0,0 +1,715 @@ +/* + * 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.mapper.dynamodb.mapper; + +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.ConditionExpressionMode; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.TransactionWriteTestData; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.TransactionWriteTestRequest; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.createHashKeyRangeKeyTable; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.createMultipleObjectTestDataForTransactionWrite; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.createSdkMapperSaveConfigTable; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.createStringHashKeyTable; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.deleteTestTable; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.extractAllObjectsFromTransactionWriteOperations; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.extractExpectedResponseObjects; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.extractObjectKeysForValidResponseSendingOperations; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.generateStringMatcherTransactWriteExpression; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.getCustomBooleanClassObject; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.getHashRangeAutoGenOtherAttributeObject; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.getStringAttributeClass; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.getStringHashAndRangeKeyObjectWithKeys; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.getTestItemObject; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.updateTransactionWriteOperationsWithNewObject; +import static software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.ObjectTableNameResolver; +import static software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.TableNameOverride; +import static software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.TableNameResolver; +import static software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.builder; +import static software.amazon.awssdk.mapper.dynamodb.TransactionWriteRequest.TransactionWriteOperation; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMappingException; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTransactionWriteExpression; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTypeConverter; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTypeConverterFactory; +import software.amazon.awssdk.mapper.dynamodb.TransactionWriteRequest; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; +import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; +import software.amazon.awssdk.mapper.dynamodb.pojos.CustomBooleanClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.HashKeyAutoGenerated; +import software.amazon.awssdk.mapper.dynamodb.pojos.HashKeyRangeKeyBothAutoGenerated; +import software.amazon.awssdk.mapper.dynamodb.pojos.HashKeyRangeKeyClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.HashRangeAndOtherAttributeAutoGen; +import software.amazon.awssdk.mapper.dynamodb.pojos.PrimaryKeysNotAutogenerated_OtherAttributeAutogenerated; +import software.amazon.awssdk.mapper.dynamodb.pojos.RangeKeyClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.StringAttributeClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.TestItem; +import software.amazon.awssdk.mapper.dynamodb.utils.AttributeAdder; +import org.junit.Test; + +public class TransactionWriteMixedTest extends TransactionsTestBase { + + // Some arbitrary big integer + private static final int ATTRIBUTE_ADDER_START = 12345678; + private static final String ADDITIONAL_ATTRIBUTE_NAME = "extra_attribute"; + private static final String STRING_ATTRIBUTE_SUB_CLASS_TABLE_NAME = "sdk-util-a34556-1c48-5d66-9c18-6872715852v7"; + + @DynamoDBTable(tableName = STRING_ATTRIBUTE_SUB_CLASS_TABLE_NAME) + public static class StringAttributeSubClass extends StringAttributeClass { + + } + + // TODO: Refactor tableNameResolvers to re-use code between multiple such instances in transactionLoad/transactionWrite integration tests + private static final TableNameResolver tableNameResolverForMissingKeyTests = new TableNameResolver() { + @Override + public String getTableName(Class clazz, DynamoDBMapperConfig config) { + if (StringAttributeClass.class.equals(clazz)) { + return sdkUtilTableName; + } + if (TestItem.class.equals(clazz)) { + return mapperSaveConfigTableName; + } + if (RangeKeyClass.class.equals(clazz)) { + return sdkRangeTableName; + } + if (HashKeyRangeKeyBothAutoGenerated.class.equals(clazz)) { + return "sdk-string-keys-" + tableNameSuffix; + } + if (HashKeyRangeKeyClass.class.equals(clazz)) { + return sdkHashRangeTestTableName; + } + throw new RuntimeException("Unable to resolve table name for input object type: " + clazz); + } + }; + + @Test + public void testWithTableNameOverride() throws InterruptedException { + String overriddenTableName = "sdk-util-" + UUID.randomUUID().toString(); + DynamoDBMapperConfig config = builder().withTableNameOverride(TableNameOverride.withTableNameReplacement(overriddenTableName)).build(); + DynamoDBMapper mapperWithTableNameOverride = new DynamoDBMapper(dynamo, config); + try { + createStringHashKeyTable(dynamo, overriddenTableName); + TransactionWriteTestRequest transactionWriteTestRequest = new TransactionWriteTestRequest() + .withPutConditionExpressionMode(ConditionExpressionMode.NONE) + .withUpdateConditionExpressionMode(ConditionExpressionMode.NONE) + .withDeleteConditionExpressionMode(ConditionExpressionMode.NONE) + .withIsMultiTable(false); + TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(mapperWithTableNameOverride, transactionWriteTestRequest); + executeAndValidateTransactionWrite(mapperWithTableNameOverride, + transactionWriteTestData.getTransactionWriteRequest(), + transactionWriteTestData.getExpectedObjectKeys(), + transactionWriteTestData.getExpectedObjects()); + } finally { + deleteTestTable(dynamo, overriddenTableName); + } + } + + // This test is needed because by default TransactionsIntegrationTestBase creates mapper with TableNameResolver + @Test + public void testWithoutAnyOverriddenConfig() throws InterruptedException { + DynamoDBMapper mapperWithoutConfig = new DynamoDBMapper(dynamo); + try { + createStringHashKeyTable(dynamo, STRING_ATTRIBUTE_SUB_CLASS_TABLE_NAME); + + StringAttributeSubClass objectToBeUpdated = getStringAttributeSubClassObject(); + StringAttributeSubClass objectToBeConditionChecked = getStringAttributeSubClassObject(); + StringAttributeSubClass objectToBeDeleted = getStringAttributeSubClassObject(); + + saveObjects(mapperWithoutConfig, objectToBeUpdated, objectToBeConditionChecked, objectToBeDeleted); + + StringAttributeSubClass objectToBePut = getStringAttributeSubClassObject(); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addPut(objectToBePut); + String updatedStringAttribute = "updatedStringAttribute"; + objectToBeUpdated.setStringAttribute(updatedStringAttribute); + transactionWriteRequest.addUpdate(objectToBeUpdated); + DynamoDBTransactionWriteExpression conditionCheckWriteExpression = + generateStringMatcherTransactWriteExpression(objectToBeConditionChecked.getStringAttribute(), + "not" + updatedStringAttribute); + transactionWriteRequest.addConditionCheck(objectToBeConditionChecked, conditionCheckWriteExpression); + transactionWriteRequest.addDelete(objectToBeDeleted); + List responseObjectKeys = Arrays.asList(objectToBePut, + objectToBeUpdated, + objectToBeDeleted); + List responseObjects = Arrays.asList(objectToBePut, + objectToBeUpdated, + null); + executeAndValidateTransactionWrite(mapperWithoutConfig, transactionWriteRequest, responseObjectKeys, responseObjects); + } finally { + deleteTestTable(dynamo, STRING_ATTRIBUTE_SUB_CLASS_TABLE_NAME); + } + } + + @Test + public void testWithAttributeTransformer() throws InterruptedException { + String overriddenTableName = "sdk-util-" + UUID.randomUUID().toString(); + DynamoDBMapperConfig config = builder().withTableNameOverride(TableNameOverride.withTableNameReplacement(overriddenTableName)).build(); + DynamoDBMapper mapperWithTransformer = new DynamoDBMapper(dynamo, config, + new AttributeAdder(ADDITIONAL_ATTRIBUTE_NAME, ATTRIBUTE_ADDER_START)); + try { + createStringHashKeyTable(dynamo, overriddenTableName); + + StringAttributeSubClass objectToBeUpdated = getStringAttributeSubClassObject(); + + saveObjects(mapperWithTransformer, objectToBeUpdated); + + Integer objectToBeUpdatedPreviousAttribute = null; + + objectToBeUpdatedPreviousAttribute = validateAdditionalAttributeAndReturnNew(overriddenTableName, objectToBeUpdated, + objectToBeUpdatedPreviousAttribute); + + StringAttributeSubClass objectToBePut = getStringAttributeSubClassObject(); + Integer objectToBePutAdditionalAttribute = null; + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addPut(objectToBePut); + String updatedStringAttribute = "updatedStringAttribute"; + objectToBeUpdated.setStringAttribute(updatedStringAttribute); + transactionWriteRequest.addUpdate(objectToBeUpdated); + List responseObjectKeys = Arrays.asList(objectToBePut, + objectToBeUpdated); + List responseObjects = Arrays.asList(objectToBePut, + objectToBeUpdated); + executeAndValidateTransactionWrite(mapperWithTransformer, transactionWriteRequest, responseObjectKeys, responseObjects); + + validateAdditionalAttributeAndReturnNew(overriddenTableName, objectToBeUpdated, objectToBeUpdatedPreviousAttribute); + validateAdditionalAttributeAndReturnNew(overriddenTableName, objectToBePut, objectToBePutAdditionalAttribute); + + } finally { + deleteTestTable(dynamo, overriddenTableName); + } + } + + private Integer validateAdditionalAttributeAndReturnNew(final String tableName, final StringAttributeSubClass object, Integer previousAttribute) { + Map key = new HashMap(); + key.put("key", AttributeValue.builder().s(object.getKey()).build()); + GetItemResponse item = dynamo.getItem(GetItemRequest.builder().tableName(tableName).key(key).build()); + assertTrue(item.item().containsKey(ADDITIONAL_ATTRIBUTE_NAME)); + Integer currentAttribute = Integer.parseInt(item.item().get(ADDITIONAL_ATTRIBUTE_NAME).n()); + assertNotEquals(currentAttribute, previousAttribute); + return currentAttribute; + } + + @Test + public void testWithObjectTableNameResolver() throws InterruptedException { + String uuid = UUID.randomUUID().toString(); + final String overriddenSdkUtilTableName = "sdk-util-" + uuid; + final String overriddenMapperSaveConfigTableName = "mapper-save-config-" + uuid; + final String sdkHashRangeTestTableName = "sdk-hash-range-test" + uuid; + DynamoDBMapperConfig config = builder().withObjectTableNameResolver(new ObjectTableNameResolver() { + @Override + public String getTableName(Object object, DynamoDBMapperConfig config) { + if (StringAttributeClass.class.equals(object.getClass())) { + return overriddenSdkUtilTableName; + } + if (TestItem.class.equals(object.getClass())) { + return overriddenMapperSaveConfigTableName; + } + if (HashKeyRangeKeyClass.class.equals(object.getClass())) { + return sdkHashRangeTestTableName; + } + throw new RuntimeException("Unable to resolve table name for input object type: " + object.getClass()); + } + }).build(); + + DynamoDBMapper mapperWithObjectTableNameResolver = new DynamoDBMapper(dynamo, config); + + try { + createStringHashKeyTable(dynamo, overriddenSdkUtilTableName); + createSdkMapperSaveConfigTable(dynamo, overriddenMapperSaveConfigTableName); + createHashKeyRangeKeyTable(dynamo, sdkHashRangeTestTableName); + TransactionWriteTestRequest transactionWriteTestRequest = new TransactionWriteTestRequest() + .withPutConditionExpressionMode(ConditionExpressionMode.NONE) + .withUpdateConditionExpressionMode(ConditionExpressionMode.NONE) + .withDeleteConditionExpressionMode(ConditionExpressionMode.NONE) + .withIsMultiTable(true); + TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(mapperWithObjectTableNameResolver, transactionWriteTestRequest); + executeAndValidateTransactionWrite(mapperWithObjectTableNameResolver, + transactionWriteTestData.getTransactionWriteRequest(), + transactionWriteTestData.getExpectedObjectKeys(), + transactionWriteTestData.getExpectedObjects()); + } finally { + deleteTestTable(dynamo, overriddenSdkUtilTableName); + deleteTestTable(dynamo, overriddenMapperSaveConfigTableName); + deleteTestTable(dynamo, sdkHashRangeTestTableName); + } + } + + @Test + public void testWithTypeConverterFactory() throws InterruptedException { + DynamoDBTypeConverterFactory typeConverterFactory = DynamoDBTypeConverterFactory.standard().override() + .with(String.class, CustomBooleanClass.CustomBoolean.class, new DynamoDBTypeConverter() { + @Override + public String convert(CustomBooleanClass.CustomBoolean bool) { + return String.valueOf(bool.getValue()); + } + + @Override + public CustomBooleanClass.CustomBoolean unconvert(String string) { + return new CustomBooleanClass.CustomBoolean(Boolean.valueOf(string)); + } + } + ).build(); + String overriddenTableName = "custom-boolean-" + UUID.randomUUID().toString(); + DynamoDBMapperConfig config = DynamoDBMapperConfig.builder().withTypeConverterFactory(typeConverterFactory) + .withTableNameOverride(TableNameOverride.withTableNameReplacement(overriddenTableName)) + .build(); + DynamoDBMapper mapperWithTypeConverterFactory = new DynamoDBMapper(dynamo, config); + + try { + createStringHashKeyTable(dynamo, overriddenTableName); + CustomBooleanClass objectToBeUpdated = getCustomBooleanClassObject(); + CustomBooleanClass objectToBeConditionChecked = getCustomBooleanClassObject(); + CustomBooleanClass objectToBeDeleted = getCustomBooleanClassObject(); + saveObjects(mapperWithTypeConverterFactory, objectToBeUpdated, objectToBeConditionChecked, objectToBeDeleted); + + StringAttributeClass objectToBePut = getStringAttributeClass(); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addPut(objectToBePut); + objectToBeUpdated.setCustomBooleanAttribute(new CustomBooleanClass.CustomBoolean(false)); + transactionWriteRequest.addUpdate(objectToBeUpdated); + DynamoDBTransactionWriteExpression conditionCheckWriteExpression = new DynamoDBTransactionWriteExpression(); + conditionCheckWriteExpression.withConditionExpression("attribute_exists(customBooleanAttribute)"); + transactionWriteRequest.addConditionCheck(objectToBeConditionChecked, conditionCheckWriteExpression); + transactionWriteRequest.addDelete(objectToBeDeleted); + + List responseObjectKeys = Arrays.asList(objectToBePut, + objectToBeUpdated, + objectToBeDeleted); + List responseObjects = Arrays.asList(objectToBePut, + objectToBeUpdated, + null); + executeAndValidateTransactionWrite(mapperWithTypeConverterFactory, transactionWriteRequest, responseObjectKeys, responseObjects); + } finally { + deleteTestTable(dynamo, overriddenTableName); + } + } + + @Test + public void testWithMissingHashKeyValueButValidRangeKeyValueForPut() { + DynamoDBMapperConfig config = DynamoDBMapperConfig.builder().withTableNameResolver(tableNameResolverForMissingKeyTests) + .build(); + DynamoDBMapper mapperWithTableNameResolver = new DynamoDBMapper(dynamo, config); + try { + TransactionWriteTestRequest transactionWriteTestRequest = new TransactionWriteTestRequest() + .withPutConditionExpressionMode(ConditionExpressionMode.NONE) + .withUpdateConditionExpressionMode(ConditionExpressionMode.NONE) + .withDeleteConditionExpressionMode(ConditionExpressionMode.NONE) + .withIsMultiTable(true); + TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(mapperWithTableNameResolver, transactionWriteTestRequest); + TestItem objectToBePut = getTestItemObject(); + objectToBePut.setHashKey(null); + + List transactionWriteOperations = transactionWriteTestData.getTransactionWriteRequest().getTransactionWriteOperations(); + updateTransactionWriteOperationsWithNewObject(transactionWriteOperations, + objectToBePut, + 0); + executeAndValidateTransactionWrite(mapperWithTableNameResolver, + transactionWriteTestData.getTransactionWriteRequest(), + transactionWriteTestData.getExpectedObjectKeys(), + transactionWriteTestData.getExpectedObjects()); + fail("Expected DynamoDBMappingException but no exception thrown"); + } catch (DynamoDBMappingException ex) { + assertEquals("TestItem[hashKey]; null or empty value for primary key", ex.getMessage()); + } + } + + @Test + public void testWithValidHashKeyValueButMissingRangeKeyValueForPut() { + DynamoDBMapperConfig config = DynamoDBMapperConfig.builder().withTableNameResolver(tableNameResolverForMissingKeyTests) + .build(); + DynamoDBMapper mapperWithTableNameResolver = new DynamoDBMapper(dynamo, config); + try { + TransactionWriteTestRequest transactionWriteTestRequest = new TransactionWriteTestRequest() + .withPutConditionExpressionMode(ConditionExpressionMode.NONE) + .withUpdateConditionExpressionMode(ConditionExpressionMode.NONE) + .withDeleteConditionExpressionMode(ConditionExpressionMode.NONE) + .withIsMultiTable(true); + TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(mapperWithTableNameResolver, transactionWriteTestRequest); + TestItem objectToBePut = getTestItemObject(); + objectToBePut.setRangeKey(null); + + List transactionWriteOperations = transactionWriteTestData.getTransactionWriteRequest().getTransactionWriteOperations(); + updateTransactionWriteOperationsWithNewObject(transactionWriteOperations, + objectToBePut, + 0); + executeAndValidateTransactionWrite(mapperWithTableNameResolver, + transactionWriteTestData.getTransactionWriteRequest(), + transactionWriteTestData.getExpectedObjectKeys(), + transactionWriteTestData.getExpectedObjects()); + fail("Expected DynamoDBMappingException but no exception thrown"); + } catch (DynamoDBMappingException ex) { + assertEquals("TestItem[rangeKey]; null or empty value for primary key", ex.getMessage()); + } + } + + @Test + public void testWithMissingHashKeyValueButValidRangeKeyValueForUpdate() { + DynamoDBMapperConfig config = DynamoDBMapperConfig.builder().withTableNameResolver(tableNameResolverForMissingKeyTests) + .build(); + DynamoDBMapper mapperWithTableNameResolver = new DynamoDBMapper(dynamo, config); + try { + TransactionWriteTestRequest transactionWriteTestRequest = new TransactionWriteTestRequest() + .withPutConditionExpressionMode(ConditionExpressionMode.NONE) + .withUpdateConditionExpressionMode(ConditionExpressionMode.NONE) + .withDeleteConditionExpressionMode(ConditionExpressionMode.NONE) + .withIsMultiTable(true); + TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(mapperWithTableNameResolver, transactionWriteTestRequest); + + TestItem objectToBeUpdated = getTestItemObject(); + objectToBeUpdated.setHashKey(null); + + List transactionWriteOperations = transactionWriteTestData.getTransactionWriteRequest().getTransactionWriteOperations(); + updateTransactionWriteOperationsWithNewObject(transactionWriteOperations, + objectToBeUpdated, + 3); + executeAndValidateTransactionWrite(mapperWithTableNameResolver, + transactionWriteTestData.getTransactionWriteRequest(), + transactionWriteTestData.getExpectedObjectKeys(), + transactionWriteTestData.getExpectedObjects()); + fail("Expected DynamoDBMappingException but no exception thrown"); + } catch (DynamoDBMappingException ex) { + assertEquals("TestItem[hashKey]; null or empty value for primary key", ex.getMessage()); + } + } + + @Test + public void testWithValidHashKeyValueButMissingRangeKeyValueForUpdate() { + DynamoDBMapperConfig config = DynamoDBMapperConfig.builder().withTableNameResolver(tableNameResolverForMissingKeyTests) + .build(); + DynamoDBMapper mapperWithTableNameResolver = new DynamoDBMapper(dynamo, config); + try { + TransactionWriteTestRequest transactionWriteTestRequest = new TransactionWriteTestRequest() + .withPutConditionExpressionMode(ConditionExpressionMode.NONE) + .withUpdateConditionExpressionMode(ConditionExpressionMode.NONE) + .withDeleteConditionExpressionMode(ConditionExpressionMode.NONE) + .withIsMultiTable(true); + TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(mapperWithTableNameResolver, transactionWriteTestRequest); + TestItem objectToBeUpdated = getTestItemObject(); + objectToBeUpdated.setRangeKey(null); + + List transactionWriteOperations = transactionWriteTestData.getTransactionWriteRequest().getTransactionWriteOperations(); + + updateTransactionWriteOperationsWithNewObject(transactionWriteOperations, + objectToBeUpdated, + 3); + executeAndValidateTransactionWrite(mapperWithTableNameResolver, + transactionWriteTestData.getTransactionWriteRequest(), + transactionWriteTestData.getExpectedObjectKeys(), + transactionWriteTestData.getExpectedObjects()); + + fail("Expected DynamoDBMappingException but no exception thrown"); + } catch (DynamoDBMappingException ex) { + assertEquals("TestItem[rangeKey]; null or empty value for primary key", ex.getMessage()); + } + } + + @Test + public void testHashKeyDoesntGetAutoGeneratedForConditionCheck() { + DynamoDBMapperConfig config = DynamoDBMapperConfig.builder().withTableNameResolver(tableNameResolverForMissingKeyTests) + .build(); + DynamoDBMapper mapperWithTableNameResolver = new DynamoDBMapper(dynamo, config); + try { + TransactionWriteTestRequest transactionWriteTestRequest = new TransactionWriteTestRequest() + .withPutConditionExpressionMode(ConditionExpressionMode.NONE) + .withUpdateConditionExpressionMode(ConditionExpressionMode.NONE) + .withDeleteConditionExpressionMode(ConditionExpressionMode.NONE) + .withIsMultiTable(true); + TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(mapperWithTableNameResolver, transactionWriteTestRequest); + HashKeyRangeKeyBothAutoGenerated objectToBeConditionChecked = getStringHashAndRangeKeyObjectWithKeys(); + objectToBeConditionChecked.setKey(null); + + List transactionWriteOperations = transactionWriteTestData.getTransactionWriteRequest().getTransactionWriteOperations(); + updateTransactionWriteOperationsWithNewObject(transactionWriteOperations, + objectToBeConditionChecked, + 4); + executeAndValidateTransactionWrite(mapperWithTableNameResolver, + transactionWriteTestData.getTransactionWriteRequest(), + transactionWriteTestData.getExpectedObjectKeys(), + transactionWriteTestData.getExpectedObjects()); + fail("Expected DynamoDBMappingException but no exception thrown"); + } catch (DynamoDBMappingException ex) { + assertEquals("HashKeyRangeKeyBothAutoGenerated[key]; no HASH key value present", ex.getMessage()); + } + } + + @Test + public void testRangeKeyDoesntGetAutoGeneratedForConditionCheck() { + DynamoDBMapperConfig config = DynamoDBMapperConfig.builder().withTableNameResolver(tableNameResolverForMissingKeyTests) + .build(); + DynamoDBMapper mapperWithTableNameResolver = new DynamoDBMapper(dynamo, config); + try { + TransactionWriteTestRequest transactionWriteTestRequest = new TransactionWriteTestRequest() + .withPutConditionExpressionMode(ConditionExpressionMode.NONE) + .withUpdateConditionExpressionMode(ConditionExpressionMode.NONE) + .withDeleteConditionExpressionMode(ConditionExpressionMode.NONE) + .withIsMultiTable(true); + TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(mapperWithTableNameResolver, transactionWriteTestRequest); + HashKeyRangeKeyBothAutoGenerated objectToBeConditionChecked = getStringHashAndRangeKeyObjectWithKeys(); + objectToBeConditionChecked.setRangeKey(null); + + List transactionWriteOperations = transactionWriteTestData.getTransactionWriteRequest().getTransactionWriteOperations(); + + updateTransactionWriteOperationsWithNewObject(transactionWriteOperations, + objectToBeConditionChecked, + 4); + executeAndValidateTransactionWrite(mapperWithTableNameResolver, + transactionWriteTestData.getTransactionWriteRequest(), + transactionWriteTestData.getExpectedObjectKeys(), + transactionWriteTestData.getExpectedObjects()); + + fail("Expected DynamoDBMappingException but no exception thrown"); + } catch (DynamoDBMappingException ex) { + assertEquals("HashKeyRangeKeyBothAutoGenerated[rangeKey]; no RANGE key value present", ex.getMessage()); + } + } + + + @Test + public void testHashKeyDoesntGetAutoGeneratedForDelete() { + DynamoDBMapperConfig config = DynamoDBMapperConfig.builder().withTableNameResolver(tableNameResolverForMissingKeyTests) + .build(); + DynamoDBMapper mapperWithTableNameResolver = new DynamoDBMapper(dynamo, config); + try { + TransactionWriteTestRequest transactionWriteTestRequest = new TransactionWriteTestRequest() + .withPutConditionExpressionMode(ConditionExpressionMode.NONE) + .withUpdateConditionExpressionMode(ConditionExpressionMode.NONE) + .withDeleteConditionExpressionMode(ConditionExpressionMode.NONE) + .withIsMultiTable(true); + TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(mapperWithTableNameResolver, transactionWriteTestRequest); + HashKeyRangeKeyBothAutoGenerated objectToBeDeleted = getStringHashAndRangeKeyObjectWithKeys(); + objectToBeDeleted.setKey(null); + + List transactionWriteOperations = transactionWriteTestData.getTransactionWriteRequest().getTransactionWriteOperations(); + updateTransactionWriteOperationsWithNewObject(transactionWriteOperations, + objectToBeDeleted, + 5); + executeAndValidateTransactionWrite(mapperWithTableNameResolver, + transactionWriteTestData.getTransactionWriteRequest(), + transactionWriteTestData.getExpectedObjectKeys(), + transactionWriteTestData.getExpectedObjects()); + fail("Expected DynamoDBMappingException but no exception thrown"); + } catch (DynamoDBMappingException ex) { + assertEquals("HashKeyRangeKeyBothAutoGenerated[key]; no HASH key value present", ex.getMessage()); + } + } + + @Test + public void testRangeKeyDoesntGetAutoGeneratedForDelete() { + DynamoDBMapperConfig config = DynamoDBMapperConfig.builder().withTableNameResolver(tableNameResolverForMissingKeyTests) + .build(); + DynamoDBMapper mapperWithTableNameResolver = new DynamoDBMapper(dynamo, config); + try { + TransactionWriteTestRequest transactionWriteTestRequest = new TransactionWriteTestRequest() + .withPutConditionExpressionMode(ConditionExpressionMode.NONE) + .withUpdateConditionExpressionMode(ConditionExpressionMode.NONE) + .withDeleteConditionExpressionMode(ConditionExpressionMode.NONE) + .withIsMultiTable(true); + TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(mapperWithTableNameResolver, transactionWriteTestRequest); + HashKeyRangeKeyBothAutoGenerated objectToBeDeleted = getStringHashAndRangeKeyObjectWithKeys(); + objectToBeDeleted.setRangeKey(null); + + List transactionWriteOperations = transactionWriteTestData.getTransactionWriteRequest().getTransactionWriteOperations(); + + updateTransactionWriteOperationsWithNewObject(transactionWriteOperations, + objectToBeDeleted, + 5); + executeAndValidateTransactionWrite(mapperWithTableNameResolver, + transactionWriteTestData.getTransactionWriteRequest(), + transactionWriteTestData.getExpectedObjectKeys(), + transactionWriteTestData.getExpectedObjects()); + + fail("Expected DynamoDBMappingException but no exception thrown"); + } catch (DynamoDBMappingException ex) { + assertEquals("HashKeyRangeKeyBothAutoGenerated[rangeKey]; no RANGE key value present", ex.getMessage()); + } + } + + @Test + public void testWithAutoGenHashKeyValueAndAutoGenHashAndRangeKeyValueOnTwoItems() { + TransactionWriteTestRequest transactionWriteTestRequest = new TransactionWriteTestRequest() + .withPutConditionExpressionMode(ConditionExpressionMode.NONE) + .withUpdateConditionExpressionMode(ConditionExpressionMode.NONE) + .withDeleteConditionExpressionMode(ConditionExpressionMode.NONE) + .withIsMultiTable(true); + TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(dynamoMapper, transactionWriteTestRequest); + + HashKeyAutoGenerated objectToBePut1 = new HashKeyAutoGenerated(); + objectToBePut1.setRangeKey("RangeKeyValue"); + objectToBePut1.setOtherAttribute("NonKeyValue"); + HashKeyRangeKeyBothAutoGenerated objectToBePut2 = getStringHashAndRangeKeyObjectWithKeys(); + objectToBePut2.setKey(null); + objectToBePut2.setRangeKey(null); + + List transactionWriteOperations = transactionWriteTestData.getTransactionWriteRequest().getTransactionWriteOperations(); + + updateTransactionWriteOperationsWithNewObject(transactionWriteOperations, + objectToBePut1, + 0); + updateTransactionWriteOperationsWithNewObject(transactionWriteOperations, + objectToBePut2, + 1); + + List allExistingObjects = extractAllObjectsFromTransactionWriteOperations(transactionWriteOperations); + List responseObjectKeys = extractObjectKeysForValidResponseSendingOperations(allExistingObjects); + List responseObjects = extractExpectedResponseObjects(allExistingObjects); + + executeAndValidateTransactionWrite(transactionWriteTestData.getTransactionWriteRequest(), + responseObjectKeys, + responseObjects); + } + + @Test + public void testWithNonKeyAttributeAutoGeneratedForPut() { + TransactionWriteTestRequest transactionWriteTestRequest = new TransactionWriteTestRequest() + .withPutConditionExpressionMode(ConditionExpressionMode.NONE) + .withUpdateConditionExpressionMode(ConditionExpressionMode.NONE) + .withDeleteConditionExpressionMode(ConditionExpressionMode.NONE) + .withIsMultiTable(true); + TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(dynamoMapper, transactionWriteTestRequest); + + PrimaryKeysNotAutogenerated_OtherAttributeAutogenerated objectToBePut = new PrimaryKeysNotAutogenerated_OtherAttributeAutogenerated(); + objectToBePut.setKey(UUID.randomUUID().toString()); + objectToBePut.setRangeKey(UUID.randomUUID().toString()); + objectToBePut.setOtherAttribute(null); + + List transactionWriteOperations = transactionWriteTestData.getTransactionWriteRequest().getTransactionWriteOperations(); + + updateTransactionWriteOperationsWithNewObject(transactionWriteOperations, + objectToBePut, + 0); + + List allExistingObjects = extractAllObjectsFromTransactionWriteOperations(transactionWriteOperations); + List responseObjectKeys = extractObjectKeysForValidResponseSendingOperations(allExistingObjects); + List responseObjects = extractExpectedResponseObjects(allExistingObjects); + + executeAndValidateTransactionWrite(transactionWriteTestData.getTransactionWriteRequest(), + responseObjectKeys, + responseObjects); + } + + @Test + public void testWithNonKeyAttributeAutoGeneratedForUpdate() { + TransactionWriteTestRequest transactionWriteTestRequest = new TransactionWriteTestRequest() + .withPutConditionExpressionMode(ConditionExpressionMode.NONE) + .withUpdateConditionExpressionMode(ConditionExpressionMode.NONE) + .withDeleteConditionExpressionMode(ConditionExpressionMode.NONE) + .withIsMultiTable(true); + TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(dynamoMapper, transactionWriteTestRequest); + + PrimaryKeysNotAutogenerated_OtherAttributeAutogenerated objectToBeUpdated = new PrimaryKeysNotAutogenerated_OtherAttributeAutogenerated(); + objectToBeUpdated.setKey(UUID.randomUUID().toString()); + objectToBeUpdated.setRangeKey(UUID.randomUUID().toString()); + objectToBeUpdated.setOtherAttribute(null); + + List transactionWriteOperations = transactionWriteTestData.getTransactionWriteRequest().getTransactionWriteOperations(); + + updateTransactionWriteOperationsWithNewObject(transactionWriteOperations, + objectToBeUpdated, + 3); + + List allExistingObjects = extractAllObjectsFromTransactionWriteOperations(transactionWriteOperations); + List responseObjectKeys = extractObjectKeysForValidResponseSendingOperations(allExistingObjects); + List responseObjects = extractExpectedResponseObjects(allExistingObjects); + + executeAndValidateTransactionWrite(transactionWriteTestData.getTransactionWriteRequest(), + responseObjectKeys, + responseObjects); + } + + @Test + public void testWithMixedKeyAndNonKeyAttributesAutoGenerated() { + TransactionWriteTestRequest transactionWriteTestRequest = new TransactionWriteTestRequest() + .withPutConditionExpressionMode(ConditionExpressionMode.NONE) + .withUpdateConditionExpressionMode(ConditionExpressionMode.NONE) + .withDeleteConditionExpressionMode(ConditionExpressionMode.NONE) + .withIsMultiTable(true); + TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(dynamoMapper, transactionWriteTestRequest); + + HashRangeAndOtherAttributeAutoGen objectToBePut = getHashRangeAutoGenOtherAttributeObject(); + objectToBePut.setDate(null); + objectToBePut.setRangeKey(null); + + List transactionWriteOperations = transactionWriteTestData.getTransactionWriteRequest().getTransactionWriteOperations(); + + updateTransactionWriteOperationsWithNewObject(transactionWriteOperations, + objectToBePut, + 0); + + List allExistingObjects = extractAllObjectsFromTransactionWriteOperations(transactionWriteOperations); + List responseObjectKeys = extractObjectKeysForValidResponseSendingOperations(allExistingObjects); + List responseObjects = extractExpectedResponseObjects(allExistingObjects); + + executeAndValidateTransactionWrite(transactionWriteTestData.getTransactionWriteRequest(), + responseObjectKeys, + responseObjects); + } + + @Test + public void testMultipleOperationsHavingNullValuedAttributes() { + + TransactionWriteTestRequest transactionWriteTestRequest = new TransactionWriteTestRequest() + .withPutConditionExpressionMode(ConditionExpressionMode.NONE) + .withUpdateConditionExpressionMode(ConditionExpressionMode.NONE) + .withDeleteConditionExpressionMode(ConditionExpressionMode.NONE) + .withIsMultiTable(true); + TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(dynamoMapper, transactionWriteTestRequest); + + StringAttributeClass objectToBePut = getStringAttributeClass(); + StringAttributeClass objectToBeUpdated = getStringAttributeClass(); + objectToBePut.setStringAttribute(null); + objectToBeUpdated.setStringAttribute(null); + + List transactionWriteOperations = transactionWriteTestData.getTransactionWriteRequest().getTransactionWriteOperations(); + + updateTransactionWriteOperationsWithNewObject(transactionWriteOperations, + objectToBePut, + 0); + + updateTransactionWriteOperationsWithNewObject(transactionWriteOperations, + objectToBeUpdated, + 3); + + List allExistingObjects = extractAllObjectsFromTransactionWriteOperations(transactionWriteOperations); + List responseObjectKeys = extractObjectKeysForValidResponseSendingOperations(allExistingObjects); + List responseObjects = extractExpectedResponseObjects(allExistingObjects); + + executeAndValidateTransactionWrite(transactionWriteTestData.getTransactionWriteRequest(), + responseObjectKeys, + responseObjects); + + } + + private static StringAttributeSubClass getStringAttributeSubClassObject() { + StringAttributeSubClass obj = new StringAttributeSubClass(); + String uuid = UUID.randomUUID().toString(); + obj.setKey(uuid); + obj.setRenamedAttribute(uuid + "1"); + obj.setStringAttribute(uuid + "2"); + return obj; + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TransactionWriteSanityTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TransactionWriteSanityTest.java new file mode 100644 index 000000000000..d078a295b9b2 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TransactionWriteSanityTest.java @@ -0,0 +1,220 @@ +/* + * 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.mapper.dynamodb.mapper; + +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.ConditionExpressionMode; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.TransactionWriteTestRequest; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.TransactionWriteTestData; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.createMultipleObjectTestDataForTransactionWrite; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.generateStringMatcherTransactWriteExpression; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.getStringAttributeClass; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.getTestItemObject; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.getHashKeyRangeKeyObject; +import static org.junit.Assert.fail; +import static org.junit.Assert.assertEquals; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTransactionWriteExpression; +import software.amazon.awssdk.mapper.dynamodb.TransactionWriteRequest; +import software.amazon.awssdk.services.dynamodb.model.TransactionCanceledException; +import software.amazon.awssdk.mapper.dynamodb.pojos.HashKeyRangeKeyClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.StringAttributeClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.TestItem; +import org.junit.Test; + +/** + * Sanity integration tests for TransactionWrite operations. + */ +public class TransactionWriteSanityTest extends TransactionsTestBase { + + @Test + public void testAllOperationsWithSingleItem() { + StringAttributeClass obj = getStringAttributeClass(); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addPut(obj); + + // Put one object + executeAndValidateTransactionWrite(transactionWriteRequest, Arrays.asList((Object) obj), Arrays.asList((Object) obj)); + + // Update the object + String updatedStringAttribute = "updatedStringAttribute"; + transactionWriteRequest = new TransactionWriteRequest(); + obj.setStringAttribute(updatedStringAttribute); + transactionWriteRequest.addUpdate(obj); + executeAndValidateTransactionWrite(transactionWriteRequest, Arrays.asList((Object) obj), Arrays.asList((Object) obj)); + + // ConditionCheck the object + transactionWriteRequest = new TransactionWriteRequest(); + DynamoDBTransactionWriteExpression writeExpression = generateStringMatcherTransactWriteExpression(updatedStringAttribute, + "not" + updatedStringAttribute); + transactionWriteRequest.addConditionCheck(obj, writeExpression); + // Succeeds + dynamoMapper.transactionWrite(transactionWriteRequest); + + // Delete + transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addDelete(obj); + List expectedObjects = new ArrayList(); + expectedObjects.add(null); + executeAndValidateTransactionWrite(transactionWriteRequest, Arrays.asList((Object) obj), expectedObjects); + } + + @Test + public void testAllOperationsWithSingleItemAndUnusualExistence() { + StringAttributeClass obj = getStringAttributeClass(); + StringAttributeClass objToPut = getStringAttributeClass(); + // To put on existing object + objToPut.setKey(obj.getKey()); + saveObject(obj); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addPut(objToPut); + + // Put one object on existing object + executeAndValidateTransactionWrite(transactionWriteRequest, Arrays.asList((Object) obj), Arrays.asList((Object) objToPut)); + + // Update non existing object + transactionWriteRequest = new TransactionWriteRequest(); + StringAttributeClass objToUpdate = getStringAttributeClass(); + transactionWriteRequest.addUpdate(objToUpdate); + executeAndValidateTransactionWrite(transactionWriteRequest, Arrays.asList((Object) objToUpdate), Arrays.asList((Object) objToUpdate)); + + // ConditionCheck non existing object + transactionWriteRequest = new TransactionWriteRequest(); + StringAttributeClass objToConditionCheck = getStringAttributeClass(); + DynamoDBTransactionWriteExpression writeExpression = generateStringMatcherTransactWriteExpression("someString", + "someOtherString"); + transactionWriteRequest.addConditionCheck(objToConditionCheck, writeExpression); + try { + dynamoMapper.transactionWrite(transactionWriteRequest); + fail("Expected TransactionCanceledException but no exception thrown"); + } catch (TransactionCanceledException tce) { + // Expected + } + + // Delete non existing object + transactionWriteRequest = new TransactionWriteRequest(); + StringAttributeClass objToDelete = getStringAttributeClass(); + transactionWriteRequest.addDelete(objToDelete); + List expectedObjects = new ArrayList(); + expectedObjects.add(null); + executeAndValidateTransactionWrite(transactionWriteRequest, Arrays.asList((Object) objToDelete), expectedObjects); + } + + @Test + public void testSingleMixedOperationsOnSingleTable() { + + StringAttributeClass objectToBeUpdated = getStringAttributeClass(); + StringAttributeClass objectToBeConditionChecked = getStringAttributeClass(); + StringAttributeClass objectToBeDeleted = getStringAttributeClass(); + + // Save it so that we can update it inside transaction + saveObject(objectToBeUpdated); + // Save it so that we can conditionCheck it inside transaction + saveObject(objectToBeConditionChecked); + // Save it so that we can delete it inside transaction + saveObject(objectToBeDeleted); + + // Save the object + StringAttributeClass objectToBePut = getStringAttributeClass(); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addPut(objectToBePut); + // Update the object + String updatedStringAttribute = "updatedStringAttribute"; + objectToBeUpdated.setStringAttribute(updatedStringAttribute); + transactionWriteRequest.addUpdate(objectToBeUpdated); + // ConditionCheck the object + DynamoDBTransactionWriteExpression conditionCheckWriteExpression = + generateStringMatcherTransactWriteExpression(objectToBeConditionChecked.getStringAttribute(), + "not" + updatedStringAttribute); + transactionWriteRequest.addConditionCheck(objectToBeConditionChecked, conditionCheckWriteExpression); + // Delete the object + transactionWriteRequest.addDelete(objectToBeDeleted); + List responseObjectKeys = Arrays.asList(objectToBePut, + objectToBeUpdated, + objectToBeDeleted); + List responseObjects = Arrays.asList(objectToBePut, + objectToBeUpdated, + null); + executeAndValidateTransactionWrite(transactionWriteRequest, responseObjectKeys, responseObjects); + } + + @Test + public void testSingleMixedOperationsOnMultipleTables() { + StringAttributeClass objectToBeUpdated = getStringAttributeClass(); + TestItem objectToBeConditionChecked = getTestItemObject(); + HashKeyRangeKeyClass objectToBeDeleted = getHashKeyRangeKeyObject(); + + // Save it so that we can update it inside transaction + saveObject(objectToBeUpdated); + // Save it so that we can conditionCheck it inside transaction + saveObject(objectToBeConditionChecked); + // Save it so that we can delete it inside transaction + saveObject(objectToBeDeleted); + + // Save the object + StringAttributeClass objectToBePut = getStringAttributeClass(); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addPut(objectToBePut); + // Update the object + String updatedStringAttribute = "updatedStringAttribute"; + objectToBeUpdated.setStringAttribute(updatedStringAttribute); + transactionWriteRequest.addUpdate(objectToBeUpdated); + // ConditionCheck the object + DynamoDBTransactionWriteExpression conditionCheckWriteExpression = + generateStringMatcherTransactWriteExpression(objectToBeConditionChecked.getStringAttribute(), + "not" + updatedStringAttribute); + transactionWriteRequest.addConditionCheck(objectToBeConditionChecked, conditionCheckWriteExpression); + + // Delete the object + transactionWriteRequest.addDelete(objectToBeDeleted); + List responseObjectKeys = Arrays.asList(objectToBePut, + objectToBeUpdated, + objectToBeDeleted); + List responseObjects = Arrays.asList(objectToBePut, + objectToBeUpdated, + null); + executeAndValidateTransactionWrite(transactionWriteRequest, responseObjectKeys, responseObjects); + } + + @Test + public void testMultipleMixedOperationsOnSingleTable() { + TransactionWriteTestRequest transactionWriteTestRequest = new TransactionWriteTestRequest() + .withPutConditionExpressionMode(ConditionExpressionMode.NONE) + .withUpdateConditionExpressionMode(ConditionExpressionMode.NONE) + .withDeleteConditionExpressionMode(ConditionExpressionMode.NONE) + .withIsMultiTable(false); + TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(dynamoMapper, transactionWriteTestRequest); + executeAndValidateTransactionWrite(transactionWriteTestData.getTransactionWriteRequest(), + transactionWriteTestData.getExpectedObjectKeys(), + transactionWriteTestData.getExpectedObjects()); + } + + @Test + public void testMultipleMixedOperationsOnMultipleTables() { + TransactionWriteTestRequest transactionWriteTestRequest = new TransactionWriteTestRequest() + .withPutConditionExpressionMode(ConditionExpressionMode.NONE) + .withUpdateConditionExpressionMode(ConditionExpressionMode.NONE) + .withDeleteConditionExpressionMode(ConditionExpressionMode.NONE) + .withIsMultiTable(true); + TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(dynamoMapper, transactionWriteTestRequest); + executeAndValidateTransactionWrite(transactionWriteTestData.getTransactionWriteRequest(), + transactionWriteTestData.getExpectedObjectKeys(), + transactionWriteTestData.getExpectedObjects()); + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TransactionWriteTableMapperTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TransactionWriteTableMapperTest.java new file mode 100644 index 000000000000..14da0135b670 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TransactionWriteTableMapperTest.java @@ -0,0 +1,343 @@ +/* + * 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.mapper.dynamodb.mapper; + +import static software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.TableNameResolver; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.ConditionExpressionMode; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.createMultipleObjectTestDataForTransactionWrite; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.createStringHashKeyTable; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.deleteTestTable; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.TransactionWriteTestData; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.TransactionWriteTestRequest; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.generateStringMatcherTransactWriteExpression; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.generateStringContainsTransactWriteExpression; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.getHashKeyRangeKeyObject; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.getStringAttributeClass; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.getTestItemObject; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMappingException; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTableMapper; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTransactionWriteExpression; +import software.amazon.awssdk.mapper.dynamodb.TransactionLoadRequest; +import software.amazon.awssdk.mapper.dynamodb.TransactionWriteRequest; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.CancellationReason; +import software.amazon.awssdk.services.dynamodb.model.TransactionCanceledException; +import software.amazon.awssdk.mapper.dynamodb.pojos.StringAttributeClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.TestItem; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.HashSet; +import java.util.Set; + +public class TransactionWriteTableMapperTest extends TransactionsTestBase { + private static final String stringAttributeClassTableName1 = "sdk-util-1e5658e0-034e-4b90-90cb-13b00cfc866a"; + private static final String stringAttributeClassTableName2 = "sdk-util-21e0905c-0b85-4675-9c0f-b0e5f6d408c9"; + + private static DynamoDBTableMapper tableMapper; + + @BeforeClass + public static void setUp() throws Exception { + TransactionsTestBase.setUp(); + tableMapper = dynamoMapper.newTableMapper(TestItem.class); + } + + @Test + public void testAllOperationsWithSingleItem() { + TestItem obj = getTestItemObject(); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addPut(obj); + + // Put one object + executeAndValidateTransactionWrite(transactionWriteRequest, Arrays.asList((Object) obj), Arrays.asList((Object) obj)); + + // Update the object + String updatedStringAttribute = "updatedStringAttribute"; + obj.setStringAttribute(updatedStringAttribute); + transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addUpdate(obj); + executeAndValidateTransactionWrite(transactionWriteRequest, Arrays.asList((Object) obj), Arrays.asList((Object) obj)); + + // ConditionCheck the object + DynamoDBTransactionWriteExpression writeExpression = generateStringMatcherTransactWriteExpression(updatedStringAttribute, + "not" + updatedStringAttribute); + transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addConditionCheck(obj, writeExpression); + // Succeeds + tableMapper.transactionWrite(transactionWriteRequest); + + // Delete + transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addDelete(obj); + List expectedObjects = new ArrayList(); + expectedObjects.add(null); + executeAndValidateTransactionWrite(transactionWriteRequest, Arrays.asList((Object) obj), expectedObjects); + } + + @Test + public void testAllOperationsWithSingleItemAndUnusualExistence() { + TestItem existingObject = getTestItemObject(); + tableMapper.save(existingObject); + TestItem objectToPut = getTestItemObject(); + objectToPut.setHashKey(existingObject.getHashKey()); + objectToPut.setRangeKey(existingObject.getRangeKey()); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addPut(objectToPut); + + // Put one object which overwrites existing object + executeAndValidateTransactionWrite(transactionWriteRequest, Arrays.asList((Object) existingObject), Arrays.asList((Object) objectToPut)); + + // Update a non-existing object + TestItem objectToUpdate = getTestItemObject(); + String updatedStringAttribute = "updatedStringAttribute"; + objectToUpdate.setStringAttribute(updatedStringAttribute); + transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addUpdate(objectToUpdate); + executeAndValidateTransactionWrite(transactionWriteRequest, Arrays.asList((Object) objectToUpdate), Arrays.asList((Object) objectToUpdate)); + + // ConditionCheck on a non-existing object + TestItem objectToConditionCheck = getTestItemObject(); + DynamoDBTransactionWriteExpression writeExpression = generateStringMatcherTransactWriteExpression("someString", + "someOtherString"); + transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addConditionCheck(objectToConditionCheck, writeExpression); + try { + tableMapper.transactionWrite(transactionWriteRequest); + fail("Expected TransactionCanceledException but no exception thrown"); + } catch (TransactionCanceledException ex) { + // Expected + } + + // Delete a non-existing object + TestItem objectToDelete = getTestItemObject(); + transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addDelete(objectToDelete); + List expectedObjects = new ArrayList(); + expectedObjects.add(null); + executeAndValidateTransactionWrite(transactionWriteRequest, Arrays.asList((Object) objectToDelete), expectedObjects); + } + + @Test + public void testWithSingleInvalidObjectTypeItem() { + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addPut(getStringAttributeClass()); + try { + tableMapper.transactionWrite(transactionWriteRequest); + fail("Expected DynamoDBMappingException but no exception thrown"); + } catch (DynamoDBMappingException ex) { + String expectedMessage = "Input object is of the classType: " + StringAttributeClass.class + " but tableMapper is declared with classType: " + + TestItem.class; + assertEquals(expectedMessage, ex.getMessage()); + } + } + + @Test + public void testMultipleMixedOperationsOnMultipleTables() throws InterruptedException { + final int numObjects = 6; + TableNameResolver tableNameResolver = new TableNameResolver() { + private int counter; + private boolean savingForTestSetup = true; + private boolean readingForTestVerification = false; + + /** + * This method gets called in three flows: + * 1. From DynamoDBMapper.save for every object except first 3 objects (because we use ConditionExpressionMode.NONE for puts so corresponding + * objects are not saved as part of test data creation) + * 2. From DynamoDBMapper.transactionWrite for every object (because we add every object as input to transactionWrite) + * 3. From DynamoDBMapper.transactionLoad for every object except objects at index 6 and 7 (because we add every object as input to + * transactionLoad for verification except conditionCheck objects) + * + * So, to keep resulting table name same across save/transactionWrite/transactionLoad operations for same object, we do the following: + * 1. If savingForTestSetup is true and counter = 0, set counter = 3 (this effectively results in getTableName calls being avoided for + * first 3 objects while saving) + * 2. If counter >= numObjects and savingForTestSetup is true (meaning we've completed save flow), we reset counter and set savingForTestSetup to false + * 3. If counter >= numObjects and savingForTestSetup is false (meaning we've completed transactionWrite flow), we reset counter and set readingForTestVerification to true + * 4. If readingForTestVerification is true and counter = 5, set counter = 8 (this effectively results in getTableName calls being avoided for + * objects at index 6 and 7 during transactionLoad) + */ + @Override + public String getTableName(Class clazz, DynamoDBMapperConfig config) { + if (!StringAttributeClass.class.equals(clazz)) { + throw new IllegalArgumentException("Input object must be of type: " + StringAttributeClass.class); + } + + if (savingForTestSetup && counter == 0) { + counter = 3; + } + + String tableName; + if (counter % 3 == 0) { + tableName = sdkUtilTableName; + } else if (counter % 3 == 1) { + tableName = stringAttributeClassTableName1; + } else { + tableName = stringAttributeClassTableName2; + } + + if (readingForTestVerification && counter == 5) { + counter = 8; + } else { + counter++; + } + + if (counter >= numObjects) { + counter = 0; + if (savingForTestSetup) { + savingForTestSetup = false; + } else { + readingForTestVerification = true; + } + } + + return tableName; + } + }; + DynamoDBMapperConfig config = DynamoDBMapperConfig.builder().withTableNameResolver(tableNameResolver).build(); + DynamoDBMapper mapperWithTableNameResolver = new DynamoDBMapper(dynamo, config); + DynamoDBTableMapper tableMapperWithTableNameResolver = mapperWithTableNameResolver.newTableMapper(StringAttributeClass.class); + + try { + createStringHashKeyTable(dynamo, stringAttributeClassTableName1); + createStringHashKeyTable(dynamo, stringAttributeClassTableName2); + TransactionWriteTestRequest testRequest = new TransactionWriteTestRequest().withPutConditionExpressionMode(ConditionExpressionMode.NONE) + .withUpdateConditionExpressionMode(ConditionExpressionMode.NONE) + .withDeleteConditionExpressionMode(ConditionExpressionMode.NONE) + .withIsMultiTable(false); + TransactionWriteTestData testData = createMultipleObjectTestDataForTransactionWrite(mapperWithTableNameResolver, testRequest); + + executeAndValidateTransactionWrite(tableMapperWithTableNameResolver, testData.getTransactionWriteRequest(), testData.getExpectedObjectKeys(), testData.getExpectedObjects()); + } finally { + deleteTestTable(dynamo, stringAttributeClassTableName1); + deleteTestTable(dynamo, stringAttributeClassTableName2); + } + } + + @Test + public void testWithMultipleMixedOperationsOfWhichSomeHaveInvalidObjectTypeItems() { + // Add operations with valid object types + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addPut(getTestItemObject()); + transactionWriteRequest.addUpdate(getTestItemObject()); + TestItem objectToConditionCheck = getTestItemObject(); + DynamoDBTransactionWriteExpression conditionCheckWriteExpression = generateStringContainsTransactWriteExpression(objectToConditionCheck.getStringAttribute()); + transactionWriteRequest.addConditionCheck(objectToConditionCheck, conditionCheckWriteExpression); + transactionWriteRequest.addDelete(getTestItemObject()); + + // Add operations with mix of valid and invalid object types + transactionWriteRequest.addPut(getStringAttributeClass()); + transactionWriteRequest.addUpdate(getTestItemObject()); + transactionWriteRequest.addUpdate(getHashKeyRangeKeyObject()); + + try { + tableMapper.transactionWrite(transactionWriteRequest); + fail("Expected DynamoDBMappingException but no exception thrown"); + } catch (DynamoDBMappingException ex) { + String expectedMessage = "Input object is of the classType: " + StringAttributeClass.class + " but tableMapper is declared with classType: " + + TestItem.class; + assertEquals(expectedMessage, ex.getMessage()); + } + } + + @Test + public void testMultipleMixedOperationsOfWhichSomeCauseTransactionCancelled() { + DynamoDBTableMapper tableMapper = dynamoMapper.newTableMapper(StringAttributeClass.class); + TransactionWriteTestRequest testRequest = new TransactionWriteTestRequest().withPutConditionExpressionMode(ConditionExpressionMode.ONE_INVALID_RETURN_VALUES_SET) + .withUpdateConditionExpressionMode(ConditionExpressionMode.ONE_INVALID_RETURN_VALUES_SET) + .withConditionCheckConditionExpressionMode(ConditionExpressionMode.ONE_INVALID_RETURN_VALUES_SET) + .withDeleteConditionExpressionMode(ConditionExpressionMode.ONE_INVALID_RETURN_VALUES_SET) + .withIsMultiTable(false); + TransactionWriteTestData testData = createMultipleObjectTestDataForTransactionWrite(dynamoMapper, testRequest); + + try { + executeAndValidateTransactionWrite(tableMapper, testData.getTransactionWriteRequest(), testData.getExpectedObjectKeys(), testData.getExpectedObjects()); + fail("Expected TransactionCanceledException but no exception thrown"); + } catch (TransactionCanceledException tce) { + // Expected + } + } + + private void validateItem(Map item, Object expectedObject) { + if (!StringAttributeClass.class.equals(expectedObject.getClass())) { + throw new IllegalArgumentException("Unsupported expectedObject type: " + expectedObject.getClass()); + } + StringAttributeClass expectedStringAttributeObject = (StringAttributeClass) expectedObject; + assertEquals(expectedStringAttributeObject.getKey(), item.get("key").s()); + assertEquals(expectedStringAttributeObject.getStringAttribute(), item.get("stringAttribute").s()); + assertEquals(expectedStringAttributeObject.getRenamedAttribute(), item.get("originalName").s()); + } + + @Override + protected void executeAndValidateTransactionWrite(TransactionWriteRequest transactionWriteRequest, + List expectedResponseObjectKeys, + List expectedResponseObjects) { + executeAndValidateTransactionWrite(tableMapper, transactionWriteRequest, expectedResponseObjectKeys, expectedResponseObjects); + } + + protected void executeAndValidateTransactionWrite(DynamoDBTableMapper tableMapper, + TransactionWriteRequest transactionWriteRequest, + List expectedResponseObjectKeys, + List expectedResponseObjects) { + tableMapper.transactionWrite(transactionWriteRequest); + int numItems = expectedResponseObjectKeys.size(); + TransactionLoadRequest transactionLoadRequest = new TransactionLoadRequest(); + for (Object objKey: expectedResponseObjectKeys) { + transactionLoadRequest.addLoad(objKey); + } + + // transactionLoad can sometimes fail due to conflict with above transactionWrite which is not yet complete, so we re-try for some time when + // that type of failure is encountered + long endTime = System.currentTimeMillis() + 30 * 1000; + List actualResponseObjects = null; + while (System.currentTimeMillis() < endTime) { + try { + actualResponseObjects = tableMapper.transactionLoad(transactionLoadRequest); + break; + } catch (TransactionCanceledException tce) { + List cancellationReasons = tce.cancellationReasons(); + Set uniqueCancellationReasonCodes = new HashSet(); + for (CancellationReason cancellationReason: cancellationReasons) { + uniqueCancellationReasonCodes.add(cancellationReason.code()); + } + if (uniqueCancellationReasonCodes.size() != 1 || !"TransactionConflict".equals(cancellationReasons.get(0).code())) { + fail("transactionLoad failed with TransactionCanceledException having non-TransactionConflict cancellation reason(s): " + tce); + } + // Sleep for some time before re-trying transactionLoad + try { + Thread.sleep(1000); + } catch (InterruptedException ie) { + fail("interrupted while waiting to re-try transactionLoad: " + ie); + } + } + } + if (actualResponseObjects == null) { + fail("Timed out while executing transactionLoad due to conflict with ongoing transactionWrite"); + } + + assertEquals(numItems, actualResponseObjects.size()); + for (int i = 0; i < numItems; i++) { + assertEquals(expectedResponseObjects.get(i), actualResponseObjects.get(i)); + } + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TransactionWriteVersionAttributeTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TransactionWriteVersionAttributeTest.java new file mode 100644 index 000000000000..115ebaa27c4e --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TransactionWriteVersionAttributeTest.java @@ -0,0 +1,620 @@ +/* + * 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.mapper.dynamodb.mapper; + +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.createMultipleObjectTestDataForTransactionWrite; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.getMultiVersionRangeKeyObject; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.getUniqueRangeKeyObject; +import static org.junit.Assert.assertEquals; + +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.mapper.dynamodb.TestObjectCreator; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTransactionWriteExpression; +import software.amazon.awssdk.mapper.dynamodb.TransactionWriteRequest; +import software.amazon.awssdk.services.dynamodb.model.TransactionCanceledException; +import software.amazon.awssdk.mapper.dynamodb.pojos.MultiVersionRangeKeyClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.RangeKeyClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +public class TransactionWriteVersionAttributeTest extends TransactionsTestBase { + + @Rule + public ExpectedException exception = ExpectedException.none(); + + private static final String VERSION_AND_CONDITION_FAILURE_MESSAGE = + "A transactional write operation may not also specify a condition " + + "expression if a versioned attribute is present on the " + + "model of the item."; + + @Test + public void testTransactionWriteForVersionedAttributeWithConditionExpressionFailsOnPut() { + RangeKeyClass obj = getUniqueRangeKeyObject(); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + DynamoDBTransactionWriteExpression writeExpression = new DynamoDBTransactionWriteExpression(); + writeExpression.withConditionExpression("SomeConditionExpression"); + + // Put + transactionWriteRequest.addPut(obj, writeExpression); + + exception.expect(SdkClientException.class); + exception.expectMessage(VERSION_AND_CONDITION_FAILURE_MESSAGE); + dynamoMapper.transactionWrite(transactionWriteRequest); + } + + @Test + public void testTransactionWriteForVersionedAttributeWithConditionExpressionFailsOnUpdate() { + RangeKeyClass obj = getUniqueRangeKeyObject(); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + DynamoDBTransactionWriteExpression writeExpression = new DynamoDBTransactionWriteExpression(); + writeExpression.withConditionExpression("SomeConditionExpression"); + // Update + transactionWriteRequest.addUpdate(obj, writeExpression); + exception.expect(SdkClientException.class); + exception.expectMessage(VERSION_AND_CONDITION_FAILURE_MESSAGE); + + dynamoMapper.transactionWrite(transactionWriteRequest); + } + + @Test + public void testTransactionWriteForVersionedAttributeWithConditionExpressionFailsOnDelete() { + RangeKeyClass obj = getUniqueRangeKeyObject(); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + DynamoDBTransactionWriteExpression writeExpression = new DynamoDBTransactionWriteExpression(); + writeExpression.withConditionExpression("SomeConditionExpression"); + // Delete + transactionWriteRequest.addDelete(obj, writeExpression); + exception.expect(SdkClientException.class); + exception.expectMessage(VERSION_AND_CONDITION_FAILURE_MESSAGE); + + dynamoMapper.transactionWrite(transactionWriteRequest); + } + + @Test + public void testTransactionWriteForVersionedAttributeWithConditionExpressionFailsOnConditionCheck() { + RangeKeyClass obj = getUniqueRangeKeyObject(); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + DynamoDBTransactionWriteExpression writeExpression = new DynamoDBTransactionWriteExpression(); + writeExpression.withConditionExpression("SomeConditionExpression"); + // ConditionCheck + transactionWriteRequest.addConditionCheck(obj, writeExpression); + exception.expect(SdkClientException.class); + exception.expectMessage(VERSION_AND_CONDITION_FAILURE_MESSAGE); + + dynamoMapper.transactionWrite(transactionWriteRequest); + } + + @Test + public void testTransactionWriteForNullVersionAttributeSucceedsOnPut() { + RangeKeyClass obj = getUniqueRangeKeyObject(); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + + // Put + transactionWriteRequest.addPut(obj); + dynamoMapper.transactionWrite(transactionWriteRequest); + // Verify that item is updated with proper version + RangeKeyClass objectOnTable = dynamoMapper.load(obj); + assertEquals(Long.valueOf(1L), objectOnTable.getVersion()); + assertEquals(obj, objectOnTable); + } + + @Test + public void testTransactionWriteForNullVersionAttributeSucceedsOnUpdate() { + RangeKeyClass obj = getUniqueRangeKeyObject(); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + + // Update + transactionWriteRequest.addUpdate(obj); + dynamoMapper.transactionWrite(transactionWriteRequest); + RangeKeyClass objectOnTable = dynamoMapper.load(obj); + // Verify that item is updated with proper version + assertEquals(Long.valueOf(1L), objectOnTable.getVersion()); + assertEquals(obj, objectOnTable); + } + + @Test + public void testTransactionWriteForNonNullVersionAttributeSucceedsOnPut() { + RangeKeyClass obj = getUniqueRangeKeyObject(); + dynamoMapper.save(obj); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + + // Put + transactionWriteRequest.addPut(obj); + dynamoMapper.transactionWrite(transactionWriteRequest); + RangeKeyClass objectOnTable = dynamoMapper.load(obj); + // Verify that item is updated with proper version + assertEquals(Long.valueOf(2L), objectOnTable.getVersion()); + assertEquals(obj, objectOnTable); + } + + @Test + public void testTransactionWriteForNonNullVersionAttributeSucceedsOnUpdate() { + RangeKeyClass obj = getUniqueRangeKeyObject(); + dynamoMapper.save(obj); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + // Update + obj = getUniqueRangeKeyObject(); + dynamoMapper.save(obj); + transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addUpdate(obj); + dynamoMapper.transactionWrite(transactionWriteRequest); + RangeKeyClass objectOnTable = dynamoMapper.load(obj); + // Verify that item is updated with proper version + assertEquals(Long.valueOf(2L), objectOnTable.getVersion()); + assertEquals(obj, objectOnTable); + } + + @Test + public void testTransactionWriteForNonNullVersionAttributeSucceedsOnDelete() { + RangeKeyClass obj = getUniqueRangeKeyObject(); + dynamoMapper.save(obj); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + + // Delete + obj = getUniqueRangeKeyObject(); + dynamoMapper.save(obj); + transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addDelete(obj); + dynamoMapper.transactionWrite(transactionWriteRequest); + // Verify that item is deleted + assertEquals(null, dynamoMapper.load(obj)); + } + + @Test + public void testTransactionWriteForHigherVersionAttributeFailsForPut() { + RangeKeyClass obj = getUniqueRangeKeyObject(); + dynamoMapper.save(obj); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + obj.setVersion(3L); + + // Put + transactionWriteRequest.addPut(obj); + try { + dynamoMapper.transactionWrite(transactionWriteRequest); + } catch (TransactionCanceledException tce) { + // Expected + } + + } + + @Test + public void testTransactionWriteForHigherVersionAttributeFailsForUpdate() { + RangeKeyClass obj = getUniqueRangeKeyObject(); + dynamoMapper.save(obj); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + obj.setVersion(3L); + // Update + transactionWriteRequest.addUpdate(obj); + try { + dynamoMapper.transactionWrite(transactionWriteRequest); + } catch (TransactionCanceledException tce) { + // Expected + } + } + + @Test + public void testTransactionWriteForHigherVersionAttributeFails() { + RangeKeyClass obj = getUniqueRangeKeyObject(); + dynamoMapper.save(obj); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + obj.setVersion(3L); + + // Delete + transactionWriteRequest.addDelete(obj); + try { + dynamoMapper.transactionWrite(transactionWriteRequest); + } catch (TransactionCanceledException tce) { + // Expected + } + + } + + @Test + public void testMultipleOperationsWithConditionAndVersionAttributeOnPut() { + TestObjectCreator.TransactionWriteTestRequest transactionWriteTestRequest = new TestObjectCreator.TransactionWriteTestRequest() + .withPutConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE) + .withUpdateConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE) + .withDeleteConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE) + .withConditionCheckConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE); + TestObjectCreator.TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(dynamoMapper, transactionWriteTestRequest); + TransactionWriteRequest transactionWriteRequest = transactionWriteTestData.getTransactionWriteRequest(); + RangeKeyClass obj = getUniqueRangeKeyObject(); + DynamoDBTransactionWriteExpression writeExpression = new DynamoDBTransactionWriteExpression(); + writeExpression.withConditionExpression("SomeConditionExpression"); + + transactionWriteRequest.addPut(obj, writeExpression); + exception.expect(SdkClientException.class); + exception.expectMessage(VERSION_AND_CONDITION_FAILURE_MESSAGE); + dynamoMapper.transactionWrite(transactionWriteRequest); + } + + @Test + public void testMultipleOperationsWithConditionAndVersionAttributeOnUpdate() { + TestObjectCreator.TransactionWriteTestRequest transactionWriteTestRequest = new TestObjectCreator.TransactionWriteTestRequest() + .withPutConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE) + .withUpdateConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE) + .withDeleteConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE) + .withConditionCheckConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE); + TestObjectCreator.TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(dynamoMapper, transactionWriteTestRequest); + TransactionWriteRequest transactionWriteRequest = transactionWriteTestData.getTransactionWriteRequest(); + RangeKeyClass obj = getUniqueRangeKeyObject(); + DynamoDBTransactionWriteExpression writeExpression = new DynamoDBTransactionWriteExpression(); + writeExpression.withConditionExpression("SomeConditionExpression"); + + transactionWriteRequest.addUpdate(obj, writeExpression); + exception.expect(SdkClientException.class); + exception.expectMessage(VERSION_AND_CONDITION_FAILURE_MESSAGE); + dynamoMapper.transactionWrite(transactionWriteRequest); + + } + + @Test + public void testMultipleOperationsWithConditionAndVersionAttributeOnConditionCheck() { + TestObjectCreator.TransactionWriteTestRequest transactionWriteTestRequest = new TestObjectCreator.TransactionWriteTestRequest() + .withPutConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE) + .withUpdateConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE) + .withDeleteConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE) + .withConditionCheckConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE); + TestObjectCreator.TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(dynamoMapper, transactionWriteTestRequest); + TransactionWriteRequest transactionWriteRequest = transactionWriteTestData.getTransactionWriteRequest(); + RangeKeyClass obj = getUniqueRangeKeyObject(); + DynamoDBTransactionWriteExpression writeExpression = new DynamoDBTransactionWriteExpression(); + writeExpression.withConditionExpression("SomeConditionExpression"); + + transactionWriteRequest.addConditionCheck(obj, writeExpression); + exception.expect(SdkClientException.class); + exception.expectMessage(VERSION_AND_CONDITION_FAILURE_MESSAGE); + dynamoMapper.transactionWrite(transactionWriteRequest); + + } + + @Test + public void testMultipleOperationsWithConditionAndVersionAttributeOnDelete() { + TestObjectCreator.TransactionWriteTestRequest transactionWriteTestRequest = new TestObjectCreator.TransactionWriteTestRequest() + .withPutConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE) + .withUpdateConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE) + .withDeleteConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE) + .withConditionCheckConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE); + TestObjectCreator.TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(dynamoMapper, transactionWriteTestRequest); + TransactionWriteRequest transactionWriteRequest = transactionWriteTestData.getTransactionWriteRequest(); + RangeKeyClass obj = getUniqueRangeKeyObject(); + DynamoDBTransactionWriteExpression writeExpression = new DynamoDBTransactionWriteExpression(); + writeExpression.withConditionExpression("SomeConditionExpression"); + + transactionWriteRequest.addDelete(obj, writeExpression); + exception.expect(SdkClientException.class); + exception.expectMessage(VERSION_AND_CONDITION_FAILURE_MESSAGE); + dynamoMapper.transactionWrite(transactionWriteRequest); + } + + @Test + public void testMultipleOperationsWithNullValuedVersionOnPut() { + TestObjectCreator.TransactionWriteTestRequest transactionWriteTestRequest = new TestObjectCreator.TransactionWriteTestRequest() + .withPutConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE) + .withUpdateConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE) + .withDeleteConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE) + .withConditionCheckConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE); + TestObjectCreator.TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(dynamoMapper, transactionWriteTestRequest); + TransactionWriteRequest transactionWriteRequest = transactionWriteTestData.getTransactionWriteRequest(); + RangeKeyClass obj = getUniqueRangeKeyObject(); + transactionWriteRequest.addPut(obj); + addVersionAttributeObjectExpectations(transactionWriteTestData, obj, obj, false /* saveObjectToTable*/); + executeAndValidateTransactionWrite(dynamoMapper, + transactionWriteRequest, + transactionWriteTestData.getExpectedObjectKeys(), + transactionWriteTestData.getExpectedObjects()); + RangeKeyClass objectOnTable = dynamoMapper.load(obj); + assertEquals(Long.valueOf(1L), objectOnTable.getVersion()); + } + + @Test + public void testMultipleOperationsWithNullValuedVersionOnUpdate() { + TestObjectCreator.TransactionWriteTestRequest transactionWriteTestRequest = new TestObjectCreator.TransactionWriteTestRequest() + .withPutConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE) + .withUpdateConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE) + .withDeleteConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE) + .withConditionCheckConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE); + TestObjectCreator.TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(dynamoMapper, transactionWriteTestRequest); + TransactionWriteRequest transactionWriteRequest = transactionWriteTestData.getTransactionWriteRequest(); + RangeKeyClass obj = getUniqueRangeKeyObject(); + transactionWriteRequest.addUpdate(obj); + addVersionAttributeObjectExpectations(transactionWriteTestData, obj, obj, false /* saveObjectToTable*/); + executeAndValidateTransactionWrite(dynamoMapper, + transactionWriteRequest, + transactionWriteTestData.getExpectedObjectKeys(), + transactionWriteTestData.getExpectedObjects()); + RangeKeyClass objectOnTable = dynamoMapper.load(obj); + assertEquals(Long.valueOf(1L), objectOnTable.getVersion()); + } + + @Test + public void testMultipleOperationsWithNullValuedVersionOnPutAndUpdate() { + TestObjectCreator.TransactionWriteTestRequest transactionWriteTestRequest = new TestObjectCreator.TransactionWriteTestRequest() + .withPutConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE) + .withUpdateConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE) + .withDeleteConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE) + .withConditionCheckConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE); + TestObjectCreator.TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(dynamoMapper, transactionWriteTestRequest); + TransactionWriteRequest transactionWriteRequest = transactionWriteTestData.getTransactionWriteRequest(); + RangeKeyClass putObj = getUniqueRangeKeyObject(); + transactionWriteRequest.addPut(putObj); + addVersionAttributeObjectExpectations(transactionWriteTestData, putObj, putObj, false /* saveObjectToTable*/); + RangeKeyClass updateObj = getUniqueRangeKeyObject(); + transactionWriteRequest.addUpdate(updateObj); + addVersionAttributeObjectExpectations(transactionWriteTestData, updateObj, updateObj, false /* saveObjectToTable*/); + executeAndValidateTransactionWrite(dynamoMapper, + transactionWriteRequest, + transactionWriteTestData.getExpectedObjectKeys(), + transactionWriteTestData.getExpectedObjects()); + RangeKeyClass putObjectOnTable = dynamoMapper.load(putObj); + assertEquals(putObj, putObjectOnTable); + RangeKeyClass updateObjectOnTable = dynamoMapper.load(updateObj); + assertEquals(updateObj, updateObjectOnTable); + } + + @Test + public void testMultipleOperationsWithNonNullValuedVersionOnPut() { + TestObjectCreator.TransactionWriteTestRequest transactionWriteTestRequest = new TestObjectCreator.TransactionWriteTestRequest() + .withPutConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE) + .withUpdateConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE) + .withDeleteConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE) + .withConditionCheckConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE); + TestObjectCreator.TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(dynamoMapper, transactionWriteTestRequest); + TransactionWriteRequest transactionWriteRequest = transactionWriteTestData.getTransactionWriteRequest(); + RangeKeyClass obj = getUniqueRangeKeyObject(); + transactionWriteRequest.addPut(obj); + addVersionAttributeObjectExpectations(transactionWriteTestData, obj, obj, true /* saveObjectToTable*/); + executeAndValidateTransactionWrite(dynamoMapper, + transactionWriteRequest, + transactionWriteTestData.getExpectedObjectKeys(), + transactionWriteTestData.getExpectedObjects()); + RangeKeyClass objectOnTable = dynamoMapper.load(obj); + assertEquals(Long.valueOf(2L), objectOnTable.getVersion()); + assertEquals(obj, objectOnTable); + } + + @Test + public void testMultipleOperationsWithNonNullValuedVersionOnUpdate() { + TestObjectCreator.TransactionWriteTestRequest transactionWriteTestRequest = new TestObjectCreator.TransactionWriteTestRequest() + .withPutConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE) + .withUpdateConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE) + .withDeleteConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE) + .withConditionCheckConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE); + TestObjectCreator.TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(dynamoMapper, transactionWriteTestRequest); + TransactionWriteRequest transactionWriteRequest = transactionWriteTestData.getTransactionWriteRequest(); + RangeKeyClass obj = getUniqueRangeKeyObject(); + transactionWriteRequest.addUpdate(obj); + addVersionAttributeObjectExpectations(transactionWriteTestData, obj, obj, true /* saveObjectToTable*/); + executeAndValidateTransactionWrite(dynamoMapper, + transactionWriteRequest, + transactionWriteTestData.getExpectedObjectKeys(), + transactionWriteTestData.getExpectedObjects()); + RangeKeyClass objectOnTable = dynamoMapper.load(obj); + assertEquals(Long.valueOf(2L), objectOnTable.getVersion()); + } + + @Test + public void testMultipleOperationsWithNonNullValuedVersionOnDelete() { + TestObjectCreator.TransactionWriteTestRequest transactionWriteTestRequest = new TestObjectCreator.TransactionWriteTestRequest() + .withPutConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE) + .withUpdateConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE) + .withDeleteConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE) + .withConditionCheckConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE); + TestObjectCreator.TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(dynamoMapper, transactionWriteTestRequest); + TransactionWriteRequest transactionWriteRequest = transactionWriteTestData.getTransactionWriteRequest(); + RangeKeyClass obj = getUniqueRangeKeyObject(); + transactionWriteRequest.addDelete(obj); + addVersionAttributeObjectExpectations(transactionWriteTestData, obj, null, true/* saveObjectToTable*/); + executeAndValidateTransactionWrite(dynamoMapper, + transactionWriteRequest, + transactionWriteTestData.getExpectedObjectKeys(), + transactionWriteTestData.getExpectedObjects()); + } + + @Test + public void testMultipleOperationsWithNonNullValuedVersionOnPutUpdateAndDelete() { + TestObjectCreator.TransactionWriteTestRequest transactionWriteTestRequest = new TestObjectCreator.TransactionWriteTestRequest() + .withPutConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE) + .withUpdateConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE) + .withDeleteConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE) + .withConditionCheckConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE); + TestObjectCreator.TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(dynamoMapper, transactionWriteTestRequest); + TransactionWriteRequest transactionWriteRequest = transactionWriteTestData.getTransactionWriteRequest(); + RangeKeyClass putObj = getUniqueRangeKeyObject(); + transactionWriteRequest.addPut(putObj); + addVersionAttributeObjectExpectations(transactionWriteTestData, putObj, putObj, true /* saveObjectToTable*/); + RangeKeyClass updateObj = getUniqueRangeKeyObject(); + transactionWriteRequest.addUpdate(updateObj); + addVersionAttributeObjectExpectations(transactionWriteTestData, updateObj, updateObj, true /* saveObjectToTable*/); + RangeKeyClass deleteObj = getUniqueRangeKeyObject(); + transactionWriteRequest.addDelete(deleteObj); + addVersionAttributeObjectExpectations(transactionWriteTestData, deleteObj, null, true /* saveObjectToTable*/); + executeAndValidateTransactionWrite(dynamoMapper, + transactionWriteRequest, + transactionWriteTestData.getExpectedObjectKeys(), + transactionWriteTestData.getExpectedObjects()); + RangeKeyClass putObjectOnTable = dynamoMapper.load(putObj); + assertEquals(putObj, putObjectOnTable); + RangeKeyClass updateObjectOnTable = dynamoMapper.load(updateObj); + assertEquals(updateObj, updateObjectOnTable); + RangeKeyClass objectOnTable = dynamoMapper.load(putObj); + assertEquals(Long.valueOf(2L), objectOnTable.getVersion()); + objectOnTable = dynamoMapper.load(updateObj); + assertEquals(Long.valueOf(2L), objectOnTable.getVersion()); + } + + @Test + public void testNullValuedMultiVersionAttributeOnPutSucceeds() { + MultiVersionRangeKeyClass obj = getMultiVersionRangeKeyObject(); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + + // Put + transactionWriteRequest.addPut(obj); + dynamoMapper.transactionWrite(transactionWriteRequest); + // Verify that item is updated with proper version + MultiVersionRangeKeyClass objectOnTable = dynamoMapper.load(obj); + assertEquals(Long.valueOf(1L), objectOnTable.getVersion()); + assertEquals(Long.valueOf(1L), objectOnTable.getVersion2()); + assertEquals(obj, objectOnTable); + } + + @Test + public void testNullValuedMultiVersionAttributeOnUpdateSucceeds() { + MultiVersionRangeKeyClass obj = getMultiVersionRangeKeyObject(); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + + // Update + transactionWriteRequest = new TransactionWriteRequest(); + obj = getMultiVersionRangeKeyObject(); + transactionWriteRequest.addUpdate(obj); + dynamoMapper.transactionWrite(transactionWriteRequest); + MultiVersionRangeKeyClass objectOnTable = dynamoMapper.load(obj); + objectOnTable = dynamoMapper.load(obj); + // Verify that item is updated with proper version + assertEquals(Long.valueOf(1L), objectOnTable.getVersion()); + assertEquals(Long.valueOf(1L), objectOnTable.getVersion2()); + assertEquals(obj, objectOnTable); + } + + @Test + public void testNonNullValuedMultiVersionAttributeOnPutSucceeds() { + MultiVersionRangeKeyClass obj = getMultiVersionRangeKeyObject(); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addPut(obj); + dynamoMapper.save(obj); + dynamoMapper.transactionWrite(transactionWriteRequest); + // Verify that item is updated with proper version + MultiVersionRangeKeyClass objectOnTable = dynamoMapper.load(obj); + assertEquals(Long.valueOf(2L), objectOnTable.getVersion()); + assertEquals(Long.valueOf(2L), objectOnTable.getVersion2()); + assertEquals(obj, objectOnTable); + } + + @Test + public void testNonNullValuedMultiVersionAttributeOnUpdateSucceeds() { + MultiVersionRangeKeyClass obj = getMultiVersionRangeKeyObject(); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addUpdate(obj); + dynamoMapper.save(obj); + dynamoMapper.transactionWrite(transactionWriteRequest); + MultiVersionRangeKeyClass objectOnTable = dynamoMapper.load(obj); + // Verify that item is updated with proper version + assertEquals(Long.valueOf(2L), objectOnTable.getVersion()); + assertEquals(Long.valueOf(2L), objectOnTable.getVersion2()); + assertEquals(obj, objectOnTable); + } + + @Test + public void testNonNullValuedMultiVersionAttributeOnDeleteSucceeds() { + MultiVersionRangeKeyClass obj = getMultiVersionRangeKeyObject(); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addDelete(obj); + dynamoMapper.save(obj); + dynamoMapper.transactionWrite(transactionWriteRequest); + MultiVersionRangeKeyClass objectOnTable = dynamoMapper.load(obj); + assertEquals(null, objectOnTable); + } + + @Test + public void testOneInvalidValuedMultiVersionAttributeOnPutFails() { + MultiVersionRangeKeyClass obj = getMultiVersionRangeKeyObject(); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addPut(obj); + dynamoMapper.save(obj); + obj.setVersion(0L); + + try { + dynamoMapper.transactionWrite(transactionWriteRequest); + } catch (TransactionCanceledException tce) { + // Expected + } + } + + @Test + public void testOneInvalidValuedMultiVersionAttributeOnUpdateFails() { + MultiVersionRangeKeyClass obj = getMultiVersionRangeKeyObject(); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addUpdate(obj); + dynamoMapper.save(obj); + obj.setVersion(2L); + try { + dynamoMapper.transactionWrite(transactionWriteRequest); + } catch (TransactionCanceledException tce) { + // Expected + } + } + + @Test + public void testOneInvalidValuedMultiVersionAttributeOnDeleteFails() { + MultiVersionRangeKeyClass obj = getMultiVersionRangeKeyObject(); + TransactionWriteRequest transactionWriteRequest = new TransactionWriteRequest(); + transactionWriteRequest.addDelete(obj); + dynamoMapper.save(obj); + obj.setVersion(0L); + try { + dynamoMapper.transactionWrite(transactionWriteRequest); + } catch (TransactionCanceledException tce) { + // Expected + } + } + + @Test + public void testNonNullValuedMultiVersionAttributesOnPutUpdateAndDelete() { + TestObjectCreator.TransactionWriteTestRequest transactionWriteTestRequest = new TestObjectCreator.TransactionWriteTestRequest() + .withPutConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE) + .withUpdateConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE) + .withDeleteConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE) + .withConditionCheckConditionExpressionMode(TestObjectCreator.ConditionExpressionMode.MULTIPLE); + TestObjectCreator.TransactionWriteTestData transactionWriteTestData = createMultipleObjectTestDataForTransactionWrite(dynamoMapper, transactionWriteTestRequest); + TransactionWriteRequest transactionWriteRequest = transactionWriteTestData.getTransactionWriteRequest(); + MultiVersionRangeKeyClass putObj = getMultiVersionRangeKeyObject(); + transactionWriteRequest.addPut(putObj); + addVersionAttributeObjectExpectations(transactionWriteTestData, putObj, putObj, true /* saveObjectToTable*/); + MultiVersionRangeKeyClass updateObj = getMultiVersionRangeKeyObject(); + transactionWriteRequest.addUpdate(updateObj); + addVersionAttributeObjectExpectations(transactionWriteTestData, updateObj, updateObj, true /* saveObjectToTable*/); + MultiVersionRangeKeyClass deleteObj = getMultiVersionRangeKeyObject(); + transactionWriteRequest.addDelete(deleteObj); + addVersionAttributeObjectExpectations(transactionWriteTestData, deleteObj, null, true /* saveObjectToTable*/); + executeAndValidateTransactionWrite(dynamoMapper, + transactionWriteRequest, + transactionWriteTestData.getExpectedObjectKeys(), + transactionWriteTestData.getExpectedObjects()); + MultiVersionRangeKeyClass putObjectOnTable = dynamoMapper.load(putObj); + assertEquals(putObj, putObjectOnTable); + MultiVersionRangeKeyClass updateObjectOnTable = dynamoMapper.load(updateObj); + assertEquals(updateObj, updateObjectOnTable); + MultiVersionRangeKeyClass objectOnTable = dynamoMapper.load(putObj); + assertEquals(Long.valueOf(2L), objectOnTable.getVersion()); + assertEquals(Long.valueOf(2L), objectOnTable.getVersion2()); + objectOnTable = dynamoMapper.load(updateObj); + assertEquals(Long.valueOf(2L), objectOnTable.getVersion()); + assertEquals(Long.valueOf(2L), objectOnTable.getVersion2()); + } + + private void addVersionAttributeObjectExpectations(TestObjectCreator.TransactionWriteTestData transactionWriteTestData, + Object obj, + Object expectedObj, + boolean saveObjectToTable) { + transactionWriteTestData.getExpectedObjectKeys().add(obj); + transactionWriteTestData.getExpectedObjects().add(expectedObj); + if (saveObjectToTable) { + dynamoMapper.save(obj); + } + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TransactionsTestBase.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TransactionsTestBase.java new file mode 100644 index 000000000000..2b95127c415c --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TransactionsTestBase.java @@ -0,0 +1,178 @@ +/* + * 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.mapper.dynamodb.mapper; + +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.createHashKeyRangeKeyTable; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.createSdkMapperSaveConfigTable; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.createSdkRangeTestTable; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.createSdkStringRangeTable; +import static software.amazon.awssdk.mapper.dynamodb.TestObjectCreator.createStringHashKeyTable; +import static software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.TableNameResolver; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.mapper.dynamodb.LocalDynamoDBTestBase; +import java.util.List; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig; +import software.amazon.awssdk.mapper.dynamodb.TransactionLoadRequest; +import software.amazon.awssdk.mapper.dynamodb.TransactionWriteRequest; +import software.amazon.awssdk.services.dynamodb.model.TransactionCanceledException; +import software.amazon.awssdk.mapper.dynamodb.pojos.HashKeyAutoGenerated; +import software.amazon.awssdk.mapper.dynamodb.pojos.HashKeyRangeKeyBothAutoGenerated; +import software.amazon.awssdk.mapper.dynamodb.pojos.HashKeyRangeKeyClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.HashRangeAndOtherAttributeAutoGen; +import software.amazon.awssdk.mapper.dynamodb.pojos.MultiVersionRangeKeyClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.PrimaryKeysNotAutogenerated_IndexKeysAutogenerated; +import software.amazon.awssdk.mapper.dynamodb.pojos.PrimaryKeysNotAutogenerated_OtherAttributeAutogenerated; +import software.amazon.awssdk.mapper.dynamodb.pojos.RangeKeyClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.SlimAttributeNamesClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.StringAttributeClass; +import software.amazon.awssdk.mapper.dynamodb.pojos.TestItem; +import org.junit.BeforeClass; + +public class TransactionsTestBase extends LocalDynamoDBTestBase { + + protected static final String tableNameSuffix = "bff5478c-929b-4952-9c91-ab0780dba849"; + protected static final String sdkUtilTableName = "sdk-util-" + tableNameSuffix; + protected static final String mapperSaveConfigTableName = "mapper-save-config-" + tableNameSuffix; + protected static final String sdkRangeTableName = "sdk-range-" + tableNameSuffix; + private static final String sdkStringRangeTableName = "sdk-string-range-" + tableNameSuffix; + protected static final String sdkHashRangeTestTableName = "sdk-hash-range-test-" + tableNameSuffix; + protected static final TableNameResolver tableNameResolver = new TableNameResolver() { + @Override + public String getTableName(Class clazz, DynamoDBMapperConfig config) { + if (StringAttributeClass.class.equals(clazz)) { + return sdkUtilTableName; + } + if (TestItem.class.equals(clazz) || SlimAttributeNamesClass.class.equals(clazz)) { + return mapperSaveConfigTableName; + } + if (RangeKeyClass.class.equals(clazz) || MultiVersionRangeKeyClass.class.equals(clazz)) { + return sdkRangeTableName; + } + if (PrimaryKeysNotAutogenerated_IndexKeysAutogenerated.class.equals(clazz) + || PrimaryKeysNotAutogenerated_OtherAttributeAutogenerated.class.equals(clazz) + || HashKeyRangeKeyBothAutoGenerated.class.equals(clazz) + || HashRangeAndOtherAttributeAutoGen.class.equals(clazz) + || HashKeyAutoGenerated.class.equals(clazz)) { + return sdkStringRangeTableName; + } + if (HashKeyRangeKeyClass.class.equals(clazz)) { + return sdkHashRangeTestTableName; + } + throw new RuntimeException("Unable to resolve table name for input object type: " + clazz); + } + }; + + protected static DynamoDBMapper dynamoMapper; + protected static DynamoDbClient dynamo; + + @BeforeClass + public static void setUp() throws Exception { + DynamoDBMapperConfig config = DynamoDBMapperConfig.builder().withTableNameResolver(tableNameResolver).build(); + + dynamo = client(); + dynamoMapper = new DynamoDBMapper(dynamo, config); + // TODO: Add table creation here only if the table is being used across child classes + createSdkMapperSaveConfigTable(dynamo, mapperSaveConfigTableName); + createSdkRangeTestTable(dynamo, sdkRangeTableName); + createStringHashKeyTable(dynamo, sdkUtilTableName); + createSdkStringRangeTable(dynamo, sdkStringRangeTableName); + createHashKeyRangeKeyTable(dynamo, sdkHashRangeTestTableName); + } + + public static void saveObjects(DynamoDBMapper mapper, Object... objs) { + for (Object obj: objs) { + saveObject(mapper, obj); + } + } + + public static void saveObject(Object obj) { + saveObject(dynamoMapper, obj); + } + + public static void saveObject(DynamoDBMapper mapper, Object obj) { + mapper.save(obj); + } + + protected void executeAndValidateTransactionLoad(TransactionLoadRequest transactionLoadRequest, List expectedObjects) { + executeAndValidateTransactionLoad(dynamoMapper, transactionLoadRequest, expectedObjects); + } + + protected void executeAndValidateTransactionLoad(DynamoDBMapper mapper, TransactionLoadRequest transactionLoadRequest, List expectedObjects) { + List responseObjects = doTransactionLoad(mapper, transactionLoadRequest); + int numItems = expectedObjects.size(); + assertEquals(numItems, responseObjects.size()); + for (int i = 0; i < numItems; i++) { + assertEquals(expectedObjects.get(i), responseObjects.get(i)); + } + } + + protected void executeAndValidateTransactionWrite(TransactionWriteRequest transactionWriteRequest, + List expectedResponseObjectKeys, + List expectedResponseObjects) { + executeAndValidateTransactionWrite(dynamoMapper, + transactionWriteRequest, + expectedResponseObjectKeys, + expectedResponseObjects); + } + + protected void executeAndValidateTransactionWrite(DynamoDBMapper mapper, + TransactionWriteRequest transactionWriteRequest, + List expectedResponseObjectKeys, + List expectedResponseObjects) { + mapper.transactionWrite(transactionWriteRequest); + int numItems = expectedResponseObjectKeys.size(); + TransactionLoadRequest transactionLoadRequest = new TransactionLoadRequest(); + for (Object objKey: expectedResponseObjectKeys) { + transactionLoadRequest.addLoad(objKey); + } + + // transactionLoad can sometimes fail due to conflict with above transactionWrite which is not yet complete, so we re-try for some time when + // that type of failure is encountered + List actualResponseObjects = doTransactionLoad(mapper, transactionLoadRequest); + + assertEquals(numItems, actualResponseObjects.size()); + for (int i = 0; i < numItems; i++) { + assertEquals(expectedResponseObjects.get(i), actualResponseObjects.get(i)); + } + } + + private List doTransactionLoad(DynamoDBMapper mapper, TransactionLoadRequest transactionLoadRequest) { + long endTime = System.currentTimeMillis() + 30 * 1000; + List response = null; + while (System.currentTimeMillis() < endTime) { + try { + response = mapper.transactionLoad(transactionLoadRequest); + break; + } catch (TransactionCanceledException tce) { + // Sleep for some time before re-trying transactionLoad + try { + Thread.sleep(1000); + } catch (InterruptedException ie) { + fail("interrupted while waiting to re-try transactionLoad: " + ie); + } + } + } + if (response == null) { + fail("Timed out while executing transactionLoad due to conflict with ongoing transactionWrite"); + } + return response; + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TypeConvertedJsonIntegrationTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TypeConvertedJsonIntegrationTest.java new file mode 100644 index 000000000000..2c10edc8eebf --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TypeConvertedJsonIntegrationTest.java @@ -0,0 +1,96 @@ +/* + * 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.mapper.dynamodb.mapper; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAutoGeneratedKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTypeConvertedJson; +import software.amazon.awssdk.mapper.dynamodb.pojos.AutoKeyAndVal; +import software.amazon.awssdk.mapper.dynamodb.pojos.Currency; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; + +/** + * Integration tests for {@code DynamoDBTypeConvertedJson}. + */ +public class TypeConvertedJsonIntegrationTest extends AbstractKeyAndValIntegrationTestCase { + + /** + * Test marshalling. + */ + @Test + public void testMarshalling() { + final KeyAndCurrency object = new KeyAndCurrency(); + object.setVal(new Currency(12.95D, "USD")); + assertBeforeAndAfterChange(false, object); + } + + /** + * Test marshalling a list. + */ + @Test + public void testListMarshalling() { + final KeyAndCurrencyList object = new KeyAndCurrencyList(); + object.setVal(new ArrayList()); + object.getVal().add(new Currency(1.99D, "CAD")); + object.getVal().add(new Currency(2.99D, "CAD")); + + final List after = assertBeforeAndAfterChange(false, object); + for (final Currency currency : after) { + assertNotNull(currency.getAmount()); + assertNotNull(currency.getUnit()); + } + } + + /** + * An object with a complex type. + */ + @DynamoDBTable(tableName="aws-java-sdk-util") + public static class KeyAndCurrency extends AutoKeyAndVal { + @DynamoDBTypeConvertedJson + public Currency getVal() { + return super.getVal(); + } + @Override + public void setVal(final Currency val) { + super.setVal(val); + } + } + + /** + * An object with a complex type. + */ + @DynamoDBTable(tableName="aws-java-sdk-util") + public static class KeyAndCurrencyList extends AutoKeyAndVal> { + public static final class CurrencyListType extends ArrayList {} + @DynamoDBTypeConvertedJson(targetType=CurrencyListType.class) + public List getVal() { + return super.getVal(); + } + @Override + public void setVal(final List val) { + super.setVal(val); + } + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TypeConvertedJsonTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TypeConvertedJsonTest.java new file mode 100644 index 000000000000..4ab5df46f7ae --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TypeConvertedJsonTest.java @@ -0,0 +1,101 @@ +/* + * 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.mapper.dynamodb.mapper; + +import java.util.HashMap; +import java.util.Map; + +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTypeConvertedJson; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; +import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class TypeConvertedJsonTest { + + private static final String HASH_KEY = "1234"; + + @Mock + private DynamoDbClient ddb; + + @Test + public void responseWithUnmappedField_IgnoresUnknownFieldAndUnmarshallsCorrectly() { + final DynamoDBMapper mapper = new DynamoDBMapper(ddb); + Map item = new HashMap<>(); + item.put("hashKey", AttributeValue.builder().s(HASH_KEY).build()); + item.put("jsonMappedPojo", AttributeValue.builder().s( + "{\"knownField\": \"knownValue\", \"unknownField\": \"unknownValue\"}").build()); + when(ddb.getItem(any(GetItemRequest.class))) + .thenReturn(GetItemResponse.builder().item(item).build()); + + final TopLevelPojo pojo = mapper.load(new TopLevelPojo().setHashKey(HASH_KEY)); + assertEquals("knownValue", pojo.getJsonMappedPojo().getKnownField()); + } + + @DynamoDBTable(tableName = "TestTable") + public static class TopLevelPojo { + + @DynamoDBHashKey + private String hashKey; + + @DynamoDBTypeConvertedJson + private JsonMappedPojo jsonMappedPojo; + + public String getHashKey() { + return hashKey; + } + + public TopLevelPojo setHashKey(String hashKey) { + this.hashKey = hashKey; + return this; + } + + public JsonMappedPojo getJsonMappedPojo() { + return jsonMappedPojo; + } + + public void setJsonMappedPojo(JsonMappedPojo jsonMappedPojo) { + this.jsonMappedPojo = jsonMappedPojo; + } + } + + public static class JsonMappedPojo { + + private String knownField; + + public String getKnownField() { + return knownField; + } + + public void setKnownField(String knownField) { + this.knownField = knownField; + } + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TypeConvertedTimestampIntegrationTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TypeConvertedTimestampIntegrationTest.java new file mode 100644 index 000000000000..eab2dd7b0d6c --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TypeConvertedTimestampIntegrationTest.java @@ -0,0 +1,308 @@ +/* + * 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.mapper.dynamodb.mapper; + +import static org.junit.Assert.assertEquals; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAutoGeneratedKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMappingException; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTypeConvertedTimestamp; +import com.amazonaws.services.dynamodbv2.model.AttributeValue; +import software.amazon.awssdk.mapper.dynamodb.pojos.AutoKeyAndVal; +import software.amazon.awssdk.mapper.dynamodb.pojos.Currency; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.Map; + +import org.junit.Test; + +/** + * Tests updating component attribute fields correctly. + */ +public class TypeConvertedTimestampIntegrationTest extends AbstractKeyAndValIntegrationTestCase { + + /** + * Test timestamp formatting. + */ + @Test + public void testCalendarTimestamp() throws Exception { + final KeyAndCalendarTimestamp object = new KeyAndCalendarTimestamp(); + object.setVal(Calendar.getInstance()); + assertBeforeAndAfterChange(false, object); + } + + /** + * Test timestamp formatting. + */ + @Test + public void testCalendarTimestampNull() { + final KeyAndCalendarTimestamp object = new KeyAndCalendarTimestamp(); + assertBeforeAndAfterChange(false, object); + } + + /** + * Test timestamp formatting. + */ + @Test + public void testDateTimestamp() throws Exception { + final KeyAndDateTimestamp object = new KeyAndDateTimestamp(); + object.setVal(Calendar.getInstance().getTime()); + assertBeforeAndAfterChange(false, object); + } + + /** + * Test timestamp formatting. + */ + @Test + public void testDateTimestampNull() { + final KeyAndDateTimestamp object = new KeyAndDateTimestamp(); + assertBeforeAndAfterChange(false, object); + } + + /** + * Test timestamp formatting. + */ + @Test + public void testLongTimestamp() throws Exception { + final KeyAndLongTimestamp object = new KeyAndLongTimestamp(); + object.setVal(Calendar.getInstance().getTime().getTime()); + assertBeforeAndAfterChange(false, object); + } + + /** + * Test timestamp formatting. + */ + @Test + public void testLongTimestampNull() { + final KeyAndLongTimestamp object = new KeyAndLongTimestamp(); + assertBeforeAndAfterChange(false, object); + } + + /** + * Test timestamp formatting. + */ + @Test + public void testEstCalendarTimestamp() throws Exception { + final KeyAndEstCalendarTimestamp object = new KeyAndEstCalendarTimestamp(); + object.setVal(Calendar.getInstance()); + assertBeforeAndAfterChange(false, object); + } + + /** + * Test timestamp formatting. + */ + @Test + public void testEstDateTimestamp() { + final KeyAndEstDateTimestamp object = new KeyAndEstDateTimestamp(); + object.setVal(Calendar.getInstance().getTime()); + assertBeforeAndAfterChange(false, object); + } + + /** + * Test timestamp formatting. + */ + @Test + public void testEstLongTimestamp() { + final KeyAndEstLongTimestamp object = new KeyAndEstLongTimestamp(); + object.setVal(Calendar.getInstance().getTime().getTime()); + assertBeforeAndAfterChange(false, object); + } + + /** + * Test timestamp formatting. + */ + @Test(expected=IllegalArgumentException.class) + public void testStringNotTimestamp() { + final KeyAndStringTimestamp object = new KeyAndStringTimestamp(); + object.setVal("NotTimestamp"); + assertBeforeAndAfterChange(false, object); + } + + /** + * Test timestamp formatting. + */ + @Test(expected=DynamoDBMappingException.class) + public void testEmptyPattern() throws Exception { + final KeyAndEmptyPattern object = new KeyAndEmptyPattern(); + object.setVal(Calendar.getInstance().getTime()); + assertBeforeAndAfterChange(false, object); + } + + /** + * Test timestamp formatting. + */ + @Test(expected=DynamoDBMappingException.class) + public void testInvalidPattern() throws Exception { + final KeyAndInvalidPattern object = new KeyAndInvalidPattern(); + object.setVal(Calendar.getInstance().getTime()); + assertBeforeAndAfterChange(false, object); + } + + /** + * An object with {@code Calendar}. + */ + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class KeyAndCalendarTimestamp extends AutoKeyAndVal { + @DynamoDBTypeConvertedTimestamp(pattern="yyyyMMddHHmmssSSSz") + public Calendar getVal() { + return super.getVal(); + } + @Override + public void setVal(final Calendar val) { + super.setVal(val); + } + } + + /** + * An object with {@code Date}. + */ + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class KeyAndDateTimestamp extends AutoKeyAndVal { + @DynamoDBTypeConvertedTimestamp(pattern="yyyyMMddHHmmssSSSz") + public Date getVal() { + return super.getVal(); + } + @Override + public void setVal(final Date val) { + super.setVal(val); + } + } + + /** + * An object with {@code Long}. + */ + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class KeyAndLongTimestamp extends AutoKeyAndVal { + @DynamoDBTypeConvertedTimestamp(pattern="yyyyMMddHHmmssSSSz") + public Long getVal() { + return super.getVal(); + } + @Override + public void setVal(final Long val) { + super.setVal(val); + } + } + + /** + * An object with {@code Calendar}. + */ + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class KeyAndEstCalendarTimestamp extends AutoKeyAndVal { + @DynamoDBTypeConvertedTimestamp(pattern="yyyyMMddHHmmssSSSz", timeZone="America/New_York") + public Calendar getVal() { + return super.getVal(); + } + @Override + public void setVal(final Calendar val) { + super.setVal(val); + } + } + + /** + * An object with {@code Date}. + */ + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class KeyAndEstDateTimestamp extends AutoKeyAndVal { + @DynamoDBTypeConvertedTimestamp(pattern="yyyyMMddHHmmssSSSz", timeZone="America/New_York") + public Date getVal() { + return super.getVal(); + } + @Override + public void setVal(final Date val) { + super.setVal(val); + } + } + + /** + * An object with {@code Long}. + */ + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class KeyAndEstLongTimestamp extends AutoKeyAndVal { + @DynamoDBTypeConvertedTimestamp(pattern="yyyyMMddHHmmssSSSz", timeZone="America/New_York") + public Long getVal() { + return super.getVal(); + } + @Override + public void setVal(final Long val) { + super.setVal(val); + } + } + + /** + * An object with {@code String}. + */ + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class KeyAndStringTimestamp extends AutoKeyAndVal { + @DynamoDBTypeConvertedTimestamp(pattern="yyyyMMddHHmmssSSSz") + public String getVal() { + return super.getVal(); + } + @Override + public void setVal(final String val) { + super.setVal(val); + } + } + + /** + * An object with {@code Date}. + */ + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class KeyAndEmptyPattern extends KeyAndDateTimestamp { + @DynamoDBTypeConvertedTimestamp(pattern="") + public Date getVal() { + return super.getVal(); + } + @Override + public void setVal(final Date val) { + super.setVal(val); + } + } + + /** + * An object with {@code Date}. + */ + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class KeyAndEmptyTimeZone extends KeyAndDateTimestamp { + @DynamoDBTypeConvertedTimestamp(pattern="yyyyMMddHHmmssSSSz", timeZone="") + public Date getVal() { + return super.getVal(); + } + @Override + public void setVal(final Date val) { + super.setVal(val); + } + } + + /** + * An object with {@code Date}. + */ + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class KeyAndInvalidPattern extends KeyAndDateTimestamp { + @DynamoDBTypeConvertedTimestamp(pattern="invalid") + public Date getVal() { + return super.getVal(); + } + @Override + public void setVal(final Date val) { + super.setVal(val); + } + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TypeConverterIntegrationTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TypeConverterIntegrationTest.java new file mode 100644 index 000000000000..df9125441176 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TypeConverterIntegrationTest.java @@ -0,0 +1,320 @@ +/* + * 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.mapper.dynamodb.mapper; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAutoGeneratedKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBDocument; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMappingException; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTypeConverted; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTypeConverter; +import com.amazonaws.services.dynamodbv2.model.AttributeValue; +import software.amazon.awssdk.mapper.dynamodb.pojos.AutoKeyAndVal; +import software.amazon.awssdk.mapper.dynamodb.pojos.Currency; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.junit.Test; + +/** + * Tests updating component attribute fields correctly. + */ +public class TypeConverterIntegrationTest extends AbstractKeyAndValIntegrationTestCase { + + /** + * Test using {@code Currency}. + */ + @Test + public void testStringCurrency() { + final KeyAndStringCurrency object = new KeyAndStringCurrency(); + object.setVal(new Currency(79.99D, "CAD")); + assertBeforeAndAfterChange(false, object); + } + + /** + * Test using {@code Currency}. + */ + @Test + public void testStringCurrencyNull() { + final KeyAndStringCurrency object = new KeyAndStringCurrency(); + assertBeforeAndAfterChange(false, object); + } + + /** + * Test using {@code Currency}. + */ + @Test(expected=DynamoDBMappingException.class) //<- does not yet support lists/maps + public void testCurrency() { + final KeyAndCurrency object = new KeyAndCurrency(); + object.setVal(new Currency(69.99D, "CAD")); + assertBeforeAndAfterChange(false, object); + } + + /** + * Test using {@code Currency}. + */ + @Test + public void testStringSetCurrency() { + final KeyAndStringSetCurrency object = new KeyAndStringSetCurrency(); + object.setVal(new HashSet(Arrays.asList(new Currency(4.99D, "USD"), new Currency(5.99D,"USD")))); + assertBeforeAndAfterChange(false, object); + } + + /** + * Test using {@code Currency}. + */ + @Test + public void testStringSetCurrencyNull() { + final KeyAndStringSetCurrency object = new KeyAndStringSetCurrency(); + assertBeforeAndAfterChange(false, object); + } + + /** + * Test using {@code Currency}. + */ + @Test + public void testDoubleCurrency() { + final KeyAndDoubleCurrency object = new KeyAndDoubleCurrency(); + object.setVal(new Currency(99.99D, "CAD")); + + final Currency currency = assertBeforeAndAfterChange(null, object); + assertEquals(object.getVal().getAmount(), currency.getAmount()); + assertEquals("USD", currency.getUnit()); + } + + /** + * Test using {@code Currency}. + */ + @Test + public void testDoubleCurrencyNull() { + final KeyAndDoubleCurrency object = new KeyAndDoubleCurrency(); + assertBeforeAndAfterChange(false, object); + } + + /** + * Test using {@code Currency}. + */ + @Test + public void testDoubleSetCurrency() { + final KeyAndDoubleSetCurrency object = new KeyAndDoubleSetCurrency(); + object.setVal(new HashSet(Arrays.asList(new Currency(28.99D, "USD"), new Currency(29.99D,"USD")))); + assertBeforeAndAfterChange(false, object); + } + + /** + * Test using {@code Currency}. + */ + @Test + public void testDoubleSetCurrencyNull() { + final KeyAndDoubleSetCurrency object = new KeyAndDoubleSetCurrency(); + assertBeforeAndAfterChange(false, object); + } + + /** + * An object with {@code Currency}. + */ + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class KeyAndStringCurrency extends AutoKeyAndVal { + @CurrencyFormat(separator="-") + public Currency getVal() { + return super.getVal(); + } + @Override + public void setVal(final Currency val) { + super.setVal(val); + } + @DynamoDBTypeConverted(converter=StringCurrencyConverter.class) + @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD,ElementType.TYPE}) + public static @interface CurrencyFormat { + String separator() default " "; + } + public static final class StringCurrencyConverter implements DynamoDBTypeConverter { + private final CurrencyFormat f; + public StringCurrencyConverter(final Class targetType, final CurrencyFormat f) { + this.f = f; + } + @Override + public String convert(final Currency object) { + return new StringBuilder().append(object.getAmount()).append(f.separator()).append(object.getUnit()).toString(); + } + @Override + public Currency unconvert(final String object) { + final String[] splits = object.split(f.separator()); + return new Currency(Double.valueOf(splits[0]), splits[1]); + } + } + } + + /** + * An object with {@code Currency}. + */ + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class KeyAndCurrency extends AutoKeyAndVal { + @DynamoDBTypeConverted(converter=NoConvertCurrencyConverter.class) + public Currency getVal() { + return super.getVal(); + } + @Override + public void setVal(final Currency val) { + super.setVal(val); + } + public static final class NoConvertCurrencyConverter implements DynamoDBTypeConverter { + @Override + public Currency convert(final Currency object) { + return object; + } + @Override + public Currency unconvert(final Currency object) { + return object; + } + } + } + + /** + * An object with {@code Currency}. + */ + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class KeyAndStringSetCurrency extends AutoKeyAndVal> { + @CurrencyFormat(separator="-") + public Set getVal() { + return super.getVal(); + } + @Override + public void setVal(final Set val) { + super.setVal(val); + } + @DynamoDBTypeConverted(converter=StringSetCurrencyConverter.class) + @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD,ElementType.TYPE}) + public static @interface CurrencyFormat { + String separator() default " "; + } + public static final class StringSetCurrencyConverter implements DynamoDBTypeConverter,Set> { + private final CurrencyFormat f; + public StringSetCurrencyConverter(final Class targetType, final CurrencyFormat f) { + this.f = f; + } + @Override + public Set convert(final Set object) { + final Set objects = new HashSet(); + for (final Currency o : object) { + objects.add(new StringBuilder().append(o.getAmount()).append(f.separator()).append(o.getUnit()).toString()); + } + return objects; + } + @Override + public Set unconvert(final Set object) { + final Set objects = new HashSet(); + for (final String o : object) { + final String[] splits = o.split(f.separator()); + objects.add(new Currency(Double.valueOf(splits[0]), splits[1])); + } + return objects; + } + } + } + + /** + * An object with {@code Currency}. + */ + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class KeyAndDoubleCurrency extends AutoKeyAndVal { + @CurrencyFormat + public Currency getVal() { + return super.getVal(); + } + @Override + public void setVal(final Currency val) { + super.setVal(val); + } + @DynamoDBTypeConverted(converter=DoubleCurrencyConverter.class) + @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD,ElementType.TYPE}) + public static @interface CurrencyFormat { + String separator() default " "; + String unit() default "USD"; + } + public static final class DoubleCurrencyConverter implements DynamoDBTypeConverter { + private final CurrencyFormat f; + public DoubleCurrencyConverter(final Class targetType, final CurrencyFormat f) { + this.f = f; + } + @Override + public Double convert(final Currency object) { + return object.getAmount(); + } + @Override + public Currency unconvert(final Double object) { + return new Currency(object, f.unit()); + } + } + } + + /** + * An object with {@code Currency}. + */ + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class KeyAndDoubleSetCurrency extends AutoKeyAndVal> { + @CurrencyFormat + public Set getVal() { + return super.getVal(); + } + @Override + public void setVal(final Set val) { + super.setVal(val); + } + @DynamoDBTypeConverted(converter=DoubleSetCurrencyConverter.class) + @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD,ElementType.TYPE}) + public static @interface CurrencyFormat { + String separator() default " "; + String unit() default "USD"; + } + public static final class DoubleSetCurrencyConverter implements DynamoDBTypeConverter,Set> { + private final CurrencyFormat f; + public DoubleSetCurrencyConverter(final Class targetType, final CurrencyFormat f) { + this.f = f; + } + @Override + public Set convert(final Set object) { + final Set objects = new HashSet(); + for (final Currency o : object) { + objects.add(o.getAmount()); + } + return objects; + } + @Override + public Set unconvert(final Set object) { + final Set objects = new HashSet(); + for (final Double o : object) { + objects.add(new Currency(o, f.unit())); + } + return objects; + } + } + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TypedIntegrationTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TypedIntegrationTest.java new file mode 100644 index 000000000000..34fd506a84c2 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/TypedIntegrationTest.java @@ -0,0 +1,150 @@ +/* + * 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.mapper.dynamodb.mapper; + +import static org.junit.Assert.assertEquals; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAutoGenerateStrategy; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAutoGeneratedDefault; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperFieldModel.DynamoDBAttributeType; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTyped; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.mapper.dynamodb.pojos.AutoKeyAndVal; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import org.junit.Test; + +/** + * Status tests for {@code Typed}. + */ +public class TypedIntegrationTest extends AbstractKeyAndValIntegrationTestCase { + + /** + * test object. + */ + @DynamoDBTable(tableName="aws-java-sdk-util") + public static class KeyAndBinaryUuid extends AutoKeyAndVal { + @DynamoDBTyped(DynamoDBAttributeType.B) + public UUID getVal() { + return super.getVal(); + } + @Override + public void setVal(final UUID val) { + super.setVal(val); + } + } + + /** + * Test the mappings. + */ + @Test + public void testMarshalling() { + final KeyAndBinaryUuid object = new KeyAndBinaryUuid(); + object.setVal(UUID.randomUUID()); + assertBeforeAndAfterChange(false, object); + } + + /** + * An object with an enumeration. + */ + @DynamoDBTable(tableName="aws-java-sdk-util") + public static class KeyAndStatus extends AutoKeyAndVal { + public static enum Status { X, Y, Z }; + @DynamoDBTyped(DynamoDBAttributeType.S) + public Status getVal() { + return super.getVal(); + } + @Override + public void setVal(final Status val) { + super.setVal(val); + } + } + + /** + * Test with a null enum val. + */ + @Test + public void testNullEnumValue() { + final KeyAndStatus object = new KeyAndStatus(); + assertBeforeAndAfterChange(false, object); + } + + /** + * Test with a non-null enum val. + */ + @Test + public void testEnumMarshalling() { + final KeyAndStatus object = new KeyAndStatus(); + object.setVal(KeyAndStatus.Status.Y); + assertBeforeAndAfterChange(false, object); + } + + /** + * An object with an enumeration. + */ + @DynamoDBTable(tableName="aws-java-sdk-util") + public static class KeyAndDefaultStatus extends KeyAndStatus { + @DynamoDBAutoGeneratedDefault("Z") + @DynamoDBTyped(DynamoDBAttributeType.S) + public Status getVal() { + return super.getVal(); + } + } + + /** + * Test with a null enum val. + */ + @Test + public void testDefaultEnumValue() { + final KeyAndDefaultStatus object = new KeyAndDefaultStatus(); + final KeyAndStatus.Status value = assertBeforeAndAfterChange(true, object); + assertEquals(KeyAndStatus.Status.Z, value); + } + + /** + * test object. + */ + @DynamoDBTable(tableName="aws-java-sdk-util") + public static class KeyAndNativeValue extends AutoKeyAndVal { + @DynamoDBTyped(DynamoDBAttributeType.M) + public AttributeValue getVal() { + return super.getVal(); + } + @Override + public void setVal(final AttributeValue val) { + super.setVal(val); + } + } + + /** + * Test the mappings. + */ + @Test + public void testNativeMap() { + final Map map = new HashMap(); + map.put("A", AttributeValue.builder().n("123").build()); + + final KeyAndNativeValue object = new KeyAndNativeValue(); + object.setVal(AttributeValue.builder().m(map).build()); + assertBeforeAndAfterChange(false, object); + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/V2CompatibleBooleansTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/V2CompatibleBooleansTest.java new file mode 100644 index 000000000000..62605b996ea7 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/V2CompatibleBooleansTest.java @@ -0,0 +1,400 @@ +/* + * 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.mapper.dynamodb.mapper; + +import java.util.HashMap; +import java.util.Map; + +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.mapper.dynamodb.ConversionSchema; +import software.amazon.awssdk.mapper.dynamodb.ConversionSchemas; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAttribute; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperFieldModel; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMappingException; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTyped; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; +import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; +import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest; +import software.amazon.awssdk.services.dynamodb.model.UpdateItemResponse; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * The default converters should be able to unmarshall both V1 booleans (numeric 0/1) and native Dynamo booleans. When using the + * standard converters booleans should be marshalled as numeric attribute values. + */ +@RunWith(MockitoJUnitRunner.class) +public class V2CompatibleBooleansTest { + + private static final String HASH_KEY = "1234"; + + @Mock + private DynamoDbClient ddb; + + /** + * Mapper with default config. + */ + private DynamoDBMapper defaultMapper; + + /** + * Mapper explicitly using {@link ConversionSchemas#V2_COMPATIBLE} + */ + private DynamoDBMapper v2CompatMapper; + + /** + * Mapper explicitly using {@link ConversionSchemas#V1} + */ + private DynamoDBMapper v1Mapper; + + /** + * Mapper explicitly using {@link ConversionSchemas#V2} + */ + private DynamoDBMapper v2Mapper; + + @Before + public void setup() { + defaultMapper = new DynamoDBMapper(ddb); + v2CompatMapper = buildMapper(ConversionSchemas.V2_COMPATIBLE); + v1Mapper = buildMapper(ConversionSchemas.V1); + v2Mapper = buildMapper(ConversionSchemas.V2); + // Just stub dummy response for all save related tests + when(ddb.updateItem(any(UpdateItemRequest.class))).thenReturn(UpdateItemResponse.builder().build()); + } + + private DynamoDBMapper buildMapper(ConversionSchema schema) { + return new DynamoDBMapper(ddb, DynamoDBMapperConfig.builder() + .withConversionSchema(schema) + .build()); + } + + /** + * Without coercion from an annotation the default mapping should marshall booleans as a number. + */ + @Test + public void saveBooleanUsingDefaultConverters_MarshallsIntoNumber() { + defaultMapper.save(new UnitTestPojo().setHashKey(HASH_KEY).setBooleanAttr(true)); + verifyAttributeUpdatedWithValue("booleanAttr", AttributeValue.builder().n("1").build()); + } + + @Test + public void saveBooleanUsingV1Schema_MarshallsIntoNumber() { + v1Mapper.save(new UnitTestPojo().setHashKey(HASH_KEY).setBooleanAttr(true)); + verifyAttributeUpdatedWithValue("booleanAttr", AttributeValue.builder().n("1").build()); + } + + @Test + public void saveBooleanUsingV2Compat_MarshallsIntoNumber() { + v2CompatMapper.save(new UnitTestPojo().setHashKey(HASH_KEY).setBooleanAttr(true)); + verifyAttributeUpdatedWithValue("booleanAttr", AttributeValue.builder().n("1").build()); + } + + @Test + public void saveBooleanUsingV2Schema_MarshallsIntoNativeBool() { + v2Mapper.save(new UnitTestPojo().setHashKey(HASH_KEY).setBooleanAttr(true)); + verifyAttributeUpdatedWithValue("booleanAttr", AttributeValue.builder().bool(true).build()); + } + + /** + * {@link software.amazon.awssdk.mapper.dynamodb.DynamoDBNativeBoolean} or {@link DynamoDBTyped} can force native + * boolean marshalling. + */ + @Test + public void saveCoercedNativeBooleanUsingDefaultConverters_MarshallsIntoNativeBool() { + saveCoercedNativeBoolean_MarshallsIntoNativeBoolean(defaultMapper); + } + + @Test + public void saveCoercedNativeBooleanUsingV1Schema_MarshallsIntoNativeBool() { + saveCoercedNativeBoolean_MarshallsIntoNativeBoolean(v1Mapper); + } + + @Test + public void saveCoercedNativeBooleanUsingV2CompatSchema_MarshallsIntoNativeBool() { + saveCoercedNativeBoolean_MarshallsIntoNativeBoolean(v2CompatMapper); + } + + @Test + public void saveCoercedNativeBooleanUsingV2_MarshallsIntoNativeBool() { + saveCoercedNativeBoolean_MarshallsIntoNativeBoolean(v2Mapper); + } + + private void saveCoercedNativeBoolean_MarshallsIntoNativeBoolean(DynamoDBMapper mapper) { + mapper.save(new UnitTestPojo().setNativeBoolean(true).setHashKey(HASH_KEY)); + verifyAttributeUpdatedWithValue("nativeBoolean", AttributeValue.builder().bool(true).build()); + } + + /** + * {@link DynamoDBTyped} can force numeric boolean conversion even when using V2 schema. + */ + @Test + public void saveCoercedNumericBooleanUsingDefaultConverters_MarshallsIntoNumericBool() { + saveCoercedNumericBoolean_MarshallsIntoNumericBoolean(defaultMapper); + } + + @Test + public void saveCoercedNumericBooleanUsingV1Schema_MarshallsIntoNumericBool() { + saveCoercedNumericBoolean_MarshallsIntoNumericBoolean(v1Mapper); + } + + @Test + public void saveCoercedNumericBooleanUsingV2CompatSchema_MarshallsIntoNumericBool() { + saveCoercedNumericBoolean_MarshallsIntoNumericBoolean(v2CompatMapper); + } + + @Test + public void saveCoercedNumericBooleanUsingV2_MarshallsIntoNumericBool() { + saveCoercedNumericBoolean_MarshallsIntoNumericBoolean(v2Mapper); + } + + private void saveCoercedNumericBoolean_MarshallsIntoNumericBoolean(DynamoDBMapper mapper) { + mapper.save(new UnitTestPojo().setNumericBoolean(true).setHashKey(HASH_KEY)); + verifyAttributeUpdatedWithValue("numericBoolean", AttributeValue.builder().n("1").build()); + } + + @Test + public void saveBooleanListUsingDefaultConverters_MarshallsIntoListOfNumbers() { + defaultMapper.save(new UnitTestPojoWithList() + .setBooleanList(Arrays.asList(Boolean.FALSE, Boolean.TRUE)) + .setHashKey(HASH_KEY)); + verifyAttributeUpdatedWithValue("booleanList", AttributeValue.builder().l(AttributeValue.builder().n("0").build(), + AttributeValue.builder().n("1").build()).build()); + } + + /** + * Verifies the mapper results in an update item call that has an update for the appropriate attribute. + * + * @param attributeName Attribute expected to be updated. + * @param expected Expected value of update action. + */ + private void verifyAttributeUpdatedWithValue(String attributeName, AttributeValue expected) { + ArgumentCaptor updateItemRequestCaptor = ArgumentCaptor.forClass(UpdateItemRequest.class); + verify(ddb).updateItem(updateItemRequestCaptor.capture()); + assertEquals(expected, updateItemRequestCaptor.getValue().attributeUpdates().get(attributeName).value()); + } + + @Test + public void loadNumericBooleanUsingDefaultConverters_UnmarshallsCorrectly() { + stubGetItemRequest("booleanAttr", AttributeValue.builder().n("1").build()); + final UnitTestPojo pojo = loadPojo(defaultMapper); + assertTrue(pojo.getBooleanAttr()); + } + + @Test + public void loadNumericBooleanUsingV1Schema_UnmarshallsCorrectly() { + stubGetItemRequest("booleanAttr", AttributeValue.builder().n("1").build()); + final UnitTestPojo pojo = loadPojo(v1Mapper); + assertTrue(pojo.getBooleanAttr()); + } + + @Test + public void loadNumericBooleanUsingV2CompatSchema_UnmarshallsCorrectly() { + stubGetItemRequest("booleanAttr", AttributeValue.builder().n("1").build()); + final UnitTestPojo pojo = loadPojo(v2CompatMapper); + assertTrue(pojo.getBooleanAttr()); + } + + @Test + public void loadNumericBooleanUsingV2_UnmarshallsCorrectly() { + stubGetItemRequest("booleanAttr", AttributeValue.builder().n("1").build()); + final UnitTestPojo pojo = loadPojo(v2Mapper); + assertTrue(pojo.getBooleanAttr()); + } + + @Test + public void loadNativeBooleanUsingDefaultConverters_UnmarshallsCorrectly() { + stubGetItemRequest("booleanAttr", AttributeValue.builder().bool(true).build()); + final UnitTestPojo pojo = loadPojo(defaultMapper); + assertTrue(pojo.getBooleanAttr()); + } + + /** + * V1 schema does not handle native bool types by default + */ + @Test(expected = DynamoDBMappingException.class) + public void loadNativeBooleanUsingV1Schema_FailsToUnmarshall() { + stubGetItemRequest("booleanAttr", AttributeValue.builder().bool(true).build()); + loadPojo(v1Mapper); + } + + /** + * Native bool support can be forced in V1 schema with @{@link DynamoDBTyped}. + */ + @Test + public void loadCoercedNativeBooleanUsingV1Schema_UnmarshallsCorrectly() { + stubGetItemRequest("nativeBoolean", AttributeValue.builder().bool(true).build()); + final UnitTestPojo pojo = loadPojo(v1Mapper); + assertTrue(pojo.getNativeBoolean()); + } + + @Test + public void loadNativeBooleanUsingV2CompatSchema_UnmarshallsCorrectly() { + stubGetItemRequest("booleanAttr", AttributeValue.builder().bool(true).build()); + final UnitTestPojo pojo = loadPojo(v2CompatMapper); + assertTrue(pojo.getBooleanAttr()); + } + + @Test + public void loadNativeBooleanUsingV2_UnmarshallsCorrectly() { + stubGetItemRequest("booleanAttr", AttributeValue.builder().bool(true).build()); + final UnitTestPojo pojo = loadPojo(v2Mapper); + assertTrue(pojo.getBooleanAttr()); + } + + @Test + public void loadNativeBooleanListUsingDefaultConverters_UnmarshallsCorrectly() { + stubGetItemRequest("booleanList", AttributeValue.builder() + .l(AttributeValue.builder().bool(true).build(), + AttributeValue.builder().bool(false).build()).build()); + final UnitTestPojoWithList pojo = loadListPojo(defaultMapper); + + assertTrue(pojo.getBooleanList().get(0)); + assertFalse(pojo.getBooleanList().get(1)); + } + + @Test + public void loadNumericBooleanListUsingDefaultConverters_UnmarshallsCorrectly() { + stubGetItemRequest("booleanList", AttributeValue.builder() + .l(AttributeValue.builder().n("1").build(), + AttributeValue.builder().n("0").build()).build()); + final UnitTestPojoWithList pojo = loadListPojo(defaultMapper); + + assertTrue(pojo.getBooleanList().get(0)); + assertFalse(pojo.getBooleanList().get(1)); + } + + private UnitTestPojoWithList loadListPojo(DynamoDBMapper mapper) { + UnitTestPojoWithList pojo = new UnitTestPojoWithList(); + pojo.setHashKey(HASH_KEY); + return mapper.load(pojo); + } + + private UnitTestPojo loadPojo(DynamoDBMapper mapper) { + return mapper.load(new UnitTestPojo().setHashKey(HASH_KEY)); + } + + /** + * Stub a call to getItem to return a result with the given attribute value in the item. + * + * @param attributeName Attribute name to return in result (in addition to hash key) + * @param attributeValue Attribute value to return in result (in addition to hash key) + */ + private void stubGetItemRequest(String attributeName, AttributeValue attributeValue) { + when(ddb.getItem(any(GetItemRequest.class))).thenReturn(createGetItemResult(attributeName, attributeValue)); + } + + /** + * Create a {@link GetItemResponse} with the hash key value ({@value #HASH_KEY} and the additional attribute. + * + * @param attributeName Additional attribute to include in created {@link GetItemResponse}. + * @param attributeValue Value of additional attribute. + */ + private GetItemResponse createGetItemResult(String attributeName, AttributeValue attributeValue) { + Map item = new HashMap<>(); + item.put("hashKey", AttributeValue.builder().s(HASH_KEY).build()); + item.put(attributeName, attributeValue); + return GetItemResponse.builder().item(item).build(); + } + + @DynamoDBTable(tableName = "UnitTestTable") + public static class UnitTestPojo { + + @DynamoDBHashKey + private String hashKey; + + @DynamoDBAttribute + private Boolean booleanAttr; + + @DynamoDBTyped(DynamoDBMapperFieldModel.DynamoDBAttributeType.BOOL) + private Boolean nativeBoolean; + + @DynamoDBTyped(DynamoDBMapperFieldModel.DynamoDBAttributeType.N) + private Boolean numericBoolean; + + + public String getHashKey() { + return hashKey; + } + + public UnitTestPojo setHashKey(String hashKey) { + this.hashKey = hashKey; + return this; + } + + public Boolean getBooleanAttr() { + return booleanAttr; + } + + public UnitTestPojo setBooleanAttr(Boolean booleanAttr) { + this.booleanAttr = booleanAttr; + return this; + } + + public Boolean getNativeBoolean() { + return nativeBoolean; + } + + public UnitTestPojo setNativeBoolean(Boolean nativeBoolean) { + this.nativeBoolean = nativeBoolean; + return this; + } + + public Boolean getNumericBoolean() { + return numericBoolean; + } + + public UnitTestPojo setNumericBoolean(Boolean numericBoolean) { + this.numericBoolean = numericBoolean; + return this; + } + + } + + public static class UnitTestPojoWithList extends UnitTestPojo { + + @DynamoDBAttribute + private List booleanList; + + public List getBooleanList() { + return booleanList; + } + + public UnitTestPojo setBooleanList(List booleanList) { + this.booleanList = booleanList; + return this; + } + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/VersionAttributeUpdateIntegrationTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/VersionAttributeUpdateIntegrationTest.java new file mode 100644 index 000000000000..043591438365 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/mapper/VersionAttributeUpdateIntegrationTest.java @@ -0,0 +1,616 @@ +/* + * 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.mapper.dynamodb.mapper; + +import static software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.ConsistentReads.CONSISTENT; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperIntegrationTestBase; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAttribute; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBDeleteExpression; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.SaveBehavior; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMappingException; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBSaveExpression; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBVersionAttribute; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException; +import software.amazon.awssdk.services.dynamodb.model.ConditionalOperator; +import software.amazon.awssdk.services.dynamodb.model.ExpectedAttributeValue; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.Test; + +/** + * Tests updating version fields correctly + */ +public class VersionAttributeUpdateIntegrationTest extends DynamoDBMapperIntegrationTestBase { + + @DynamoDBTable(tableName = "aws-java-sdk-util") + public static class VersionFieldBaseClass { + + protected String key; + protected String normalStringAttribute; + + @DynamoDBHashKey + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + @DynamoDBAttribute + public String getNormalStringAttribute() { + return normalStringAttribute; + } + + public void setNormalStringAttribute(String normalStringAttribute) { + this.normalStringAttribute = normalStringAttribute; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((key == null) ? 0 : key.hashCode()); + result = prime * result + ((normalStringAttribute == null) ? 0 : normalStringAttribute.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) + return true; + if ( obj == null ) + return false; + if ( getClass() != obj.getClass() ) + return false; + VersionFieldBaseClass other = (VersionFieldBaseClass) obj; + if ( key == null ) { + if ( other.key != null ) + return false; + } else if ( !key.equals(other.key) ) + return false; + if ( normalStringAttribute == null ) { + if ( other.normalStringAttribute != null ) + return false; + } else if ( !normalStringAttribute.equals(other.normalStringAttribute) ) + return false; + return true; + } + } + + public static class StringVersionField extends VersionFieldBaseClass { + + private String version; + + @DynamoDBVersionAttribute + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((version == null) ? 0 : version.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) + return true; + if ( !super.equals(obj) ) + return false; + if ( getClass() != obj.getClass() ) + return false; + StringVersionField other = (StringVersionField) obj; + if ( version == null ) { + if ( other.version != null ) + return false; + } else if ( !version.equals(other.version) ) + return false; + return true; + } + } + + @Test(expected = DynamoDBMappingException.class) + public void testStringVersion() throws Exception { + List objs = new ArrayList(); + for ( int i = 0; i < 5; i++ ) { + StringVersionField obj = getUniqueObject(new StringVersionField()); + objs.add(obj); + } + + // Saving new objects with a null version field should populate it + DynamoDBMapper util = new DynamoDBMapper(dynamo); + for ( StringVersionField obj : objs ) { + assertNull(obj.getVersion()); + util.save(obj); + assertNotNull(obj.getVersion()); + assertEquals(obj, util.load(StringVersionField.class, obj.getKey())); + } + } + + public static class BigIntegerVersionField extends VersionFieldBaseClass { + + private BigInteger version; + + @DynamoDBVersionAttribute + public BigInteger getVersion() { + return version; + } + + public void setVersion(BigInteger version) { + this.version = version; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((version == null) ? 0 : version.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) + return true; + if ( !super.equals(obj) ) + return false; + if ( getClass() != obj.getClass() ) + return false; + BigIntegerVersionField other = (BigIntegerVersionField) obj; + if ( version == null ) { + if ( other.version != null ) + return false; + } else if ( !version.equals(other.version) ) + return false; + return true; + } + + @Override + public String toString() { + return "BigIntegerVersionField [version=" + version + ", key=" + key + ", normalStringAttribute=" + + normalStringAttribute + "]"; + } + } + + @Test + public void testBigIntegerVersion() { + List objs = new ArrayList(); + for ( int i = 0; i < 5; i++ ) { + BigIntegerVersionField obj = getUniqueObject(new BigIntegerVersionField()); + objs.add(obj); + } + + // Saving new objects with a null version field should populate it + DynamoDBMapper util = new DynamoDBMapper(dynamo, DynamoDBMapperConfig.builder() + .withConsistentReads(CONSISTENT) + .build()); + for ( BigIntegerVersionField obj : objs ) { + assertNull(obj.getVersion()); + util.save(obj); + assertNotNull(obj.getVersion()); + + assertEquals(obj, util.load(BigIntegerVersionField.class, obj.getKey())); + } + + for ( BigIntegerVersionField obj : objs ) { + BigIntegerVersionField replacement = getUniqueObject(new BigIntegerVersionField()); + replacement.setKey(obj.getKey()); + replacement.setVersion(obj.getVersion()); + + util.save(replacement); + // The version field should have changed in memory + assertFalse(obj.getVersion().equals(replacement.getVersion())); + + BigIntegerVersionField loadedObject = util.load(BigIntegerVersionField.class, obj.getKey()); + assertEquals(replacement, loadedObject); + + // Trying to update the object again should trigger a concurrency + // exception + try { + util.save(obj); + fail("Should have thrown an exception"); + } catch ( Exception expected ) { + } + + // Now try again overlaying the correct version number by using a saveExpression + // this should not throw the conditional check failed exception + try { + DynamoDBSaveExpression saveExpression = new DynamoDBSaveExpression(); + Map expected = new HashMap(); + ExpectedAttributeValue expectedVersion = ExpectedAttributeValue.builder() + .value(AttributeValue.builder() + .n(obj.getVersion().add(BigInteger.valueOf(1)).toString()).build()).build(); + expected.put("version", expectedVersion); + saveExpression.setExpected(expected); + util.save(obj, saveExpression); + } catch ( Exception expected ) { + fail("This should succeed, version was updated."); + } + } + } + + public static final class IntegerVersionField extends VersionFieldBaseClass { + + private Integer notCalledVersion; + + // Making sure that we can substitute attribute names as necessary + @DynamoDBVersionAttribute(attributeName = "version") + public Integer getNotCalledVersion() { + return notCalledVersion; + } + + public void setNotCalledVersion(Integer notCalledVersion) { + this.notCalledVersion = notCalledVersion; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((notCalledVersion == null) ? 0 : notCalledVersion.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) + return true; + if ( !super.equals(obj) ) + return false; + if ( getClass() != obj.getClass() ) + return false; + IntegerVersionField other = (IntegerVersionField) obj; + if ( notCalledVersion == null ) { + if ( other.notCalledVersion != null ) + return false; + } else if ( !notCalledVersion.equals(other.notCalledVersion) ) + return false; + return true; + } + } + + @Test + public void testIntegerVersion() { + List objs = new ArrayList(); + for ( int i = 0; i < 5; i++ ) { + IntegerVersionField obj = getUniqueObject(new IntegerVersionField()); + objs.add(obj); + } + + // Saving new objects with a null version field should populate it + DynamoDBMapper util = new DynamoDBMapper(dynamo); + for ( IntegerVersionField obj : objs ) { + assertNull(obj.getNotCalledVersion()); + util.save(obj); + assertNotNull(obj.getNotCalledVersion()); + + assertEquals(obj, util.load(IntegerVersionField.class, obj.getKey())); + } + + for ( IntegerVersionField obj : objs ) { + IntegerVersionField replacement = getUniqueObject(new IntegerVersionField()); + replacement.setKey(obj.getKey()); + replacement.setNotCalledVersion(obj.getNotCalledVersion()); + + util.save(replacement); + // The version field should have changed in memory + assertFalse(obj.getNotCalledVersion().equals(replacement.getNotCalledVersion())); + + IntegerVersionField loadedObject = util.load(IntegerVersionField.class, obj.getKey()); + assertEquals(replacement, loadedObject); + + // Trying to update the object again should trigger a concurrency + // exception + try { + util.save(obj); + fail("Should have thrown an exception"); + } catch ( Exception expected ) { + } + + // Trying to delete the object should also fail + try { + util.delete(obj); + fail("Should have thrown an exception"); + } catch ( Exception expected ) { + } + + // But specifying CLOBBER will allow deletion + util.save(obj, new DynamoDBMapperConfig(SaveBehavior.CLOBBER)); + + // Trying to delete with the wrong version should fail + try { + //version is now 2 in db, set object version to 3. + obj.setNotCalledVersion(3); + util.delete(obj); + fail("Should have thrown an exception"); + } catch ( Exception expected ) { + } + + // Now try deleting again overlaying the correct version number by using a deleteExpression + // this should not throw the conditional check failed exception + try { + DynamoDBDeleteExpression deleteExpression = new DynamoDBDeleteExpression(); + Map expected = new HashMap(); + ExpectedAttributeValue expectedVersion = ExpectedAttributeValue.builder() + .value(AttributeValue.builder() + .n("2").build()).build(); //version is still 2 in db + expected.put("version", expectedVersion); + deleteExpression.setExpected(expected); + util.delete(obj, deleteExpression); + } catch ( Exception expected ) { + fail("This should succeed, version was updated."); + } + } + } + + /** + * Tests providing additional expected conditions when saving and deleting + * item with versioned fields. + */ + @Test + public void testVersionedAttributeWithUserProvidedExpectedConditions() { + DynamoDBMapper mapper = new DynamoDBMapper(dynamo); + IntegerVersionField versionedObject = getUniqueObject(new IntegerVersionField()); + assertNull(versionedObject.getNotCalledVersion()); + + // Add additional expected conditions via DynamoDBSaveExpression. + // Expected conditions joined by AND are compatible with the conditions + // for auto-generated keys. + DynamoDBSaveExpression saveExpression = new DynamoDBSaveExpression() + .withExpected(Collections.singletonMap( + "otherAttribute", ExpectedAttributeValue.builder().exists(false).build())) + .withConditionalOperator(ConditionalOperator.AND); + // The save should succeed since the user provided conditions are joined by AND. + mapper.save(versionedObject, saveExpression); + // The version field should be populated + assertNotNull(versionedObject.getNotCalledVersion()); + IntegerVersionField other = mapper.load(IntegerVersionField.class, versionedObject.getKey()); + assertEquals(other, versionedObject); + + // delete should also work + DynamoDBDeleteExpression deleteExpression = new DynamoDBDeleteExpression() + .withExpected(Collections.singletonMap( + "otherAttribute", ExpectedAttributeValue.builder().exists(false).build())) + .withConditionalOperator(ConditionalOperator.AND); + mapper.delete(versionedObject, deleteExpression); + + // Change the conditional operator to OR. + // IllegalArgumentException is expected since the additional expected + // conditions cannot be joined with the conditions for auto-generated + // keys. + saveExpression.setConditionalOperator(ConditionalOperator.OR); + deleteExpression.setConditionalOperator(ConditionalOperator.OR); + try { + mapper.save(getUniqueObject(new IntegerVersionField()), saveExpression); + } catch (IllegalArgumentException expected) {} + try { + mapper.delete(getUniqueObject(new IntegerVersionField()), deleteExpression); + } catch (IllegalArgumentException expected) {} + + // User-provided OR conditions should work if they completely override + // the generated conditions for the version field. + Map goodConditions = new HashMap(); + goodConditions.put("otherAttribute", ExpectedAttributeValue.builder().exists(false).build()); + goodConditions.put("version", ExpectedAttributeValue.builder().exists(false).build()); + Map badConditions = new HashMap(); + badConditions.put("otherAttribute", ExpectedAttributeValue.builder().value(AttributeValue.builder().s("non-existent-value").build()).build()); + badConditions.put("version", ExpectedAttributeValue.builder().value(AttributeValue.builder().n("-1").build()).build()); + + IntegerVersionField newObj = getUniqueObject(new IntegerVersionField()); + saveExpression.setExpected(badConditions); + try { + mapper.save(newObj, saveExpression); + } catch (ConditionalCheckFailedException expected) {} + + saveExpression.setExpected(goodConditions); + mapper.save(newObj, saveExpression); + + deleteExpression.setExpected(badConditions); + try { + mapper.delete(newObj, deleteExpression); + } catch (ConditionalCheckFailedException expected) {} + + deleteExpression.setExpected(goodConditions); + mapper.delete(newObj, deleteExpression); + } + + public static final class ByteVersionField extends VersionFieldBaseClass { + + private Byte version; + + @DynamoDBVersionAttribute + public Byte getVersion() { + return version; + } + + public void setVersion(Byte version) { + this.version = version; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((version == null) ? 0 : version.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) + return true; + if ( !super.equals(obj) ) + return false; + if ( getClass() != obj.getClass() ) + return false; + ByteVersionField other = (ByteVersionField) obj; + if ( version == null ) { + if ( other.version != null ) + return false; + } else if ( !version.equals(other.version) ) + return false; + return true; + } + } + + @Test + public void testByteVersion() { + List objs = new ArrayList(); + for ( int i = 0; i < 5; i++ ) { + ByteVersionField obj = getUniqueObject(new ByteVersionField()); + objs.add(obj); + } + + // Saving new objects with a null version field should populate it + DynamoDBMapper util = new DynamoDBMapper(dynamo); + for ( ByteVersionField obj : objs ) { + assertNull(obj.getVersion()); + util.save(obj); + assertNotNull(obj.getVersion()); + + assertEquals(obj, util.load(ByteVersionField.class, obj.getKey())); + } + + for ( ByteVersionField obj : objs ) { + ByteVersionField replacement = getUniqueObject(new ByteVersionField()); + replacement.setKey(obj.getKey()); + replacement.setVersion(obj.getVersion()); + + util.save(replacement); + // The version field should have changed in memory + assertFalse(obj.getVersion().equals(replacement.getVersion())); + + ByteVersionField loadedObject = util.load(ByteVersionField.class, obj.getKey()); + assertEquals(replacement, loadedObject); + + // Trying to update the object again should trigger a concurrency + // exception + try { + util.save(obj); + fail("Should have thrown an exception"); + } catch ( Exception expected ) { + } + } + } + + public static final class LongVersionField extends VersionFieldBaseClass { + + private Long version; + + @DynamoDBVersionAttribute + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((version == null) ? 0 : version.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) + return true; + if ( !super.equals(obj) ) + return false; + if ( getClass() != obj.getClass() ) + return false; + LongVersionField other = (LongVersionField) obj; + if ( version == null ) { + if ( other.version != null ) + return false; + } else if ( !version.equals(other.version) ) + return false; + return true; + } + } + + @Test + public void testLongVersion() { + List objs = new ArrayList(); + for ( int i = 0; i < 5; i++ ) { + LongVersionField obj = getUniqueObject(new LongVersionField()); + objs.add(obj); + } + + // Saving new objects with a null version field should populate it + DynamoDBMapper util = new DynamoDBMapper(dynamo); + for ( LongVersionField obj : objs ) { + assertNull(obj.getVersion()); + util.save(obj); + assertNotNull(obj.getVersion()); + + assertEquals(obj, util.load(LongVersionField.class, obj.getKey())); + } + + for ( LongVersionField obj : objs ) { + LongVersionField replacement = getUniqueObject(new LongVersionField()); + replacement.setKey(obj.getKey()); + replacement.setVersion(obj.getVersion()); + + util.save(replacement); + // The version field should have changed in memory + assertFalse(obj.getVersion().equals(replacement.getVersion())); + + LongVersionField loadedObject = util.load(LongVersionField.class, obj.getKey()); + assertEquals(replacement, loadedObject); + + // Trying to update the object again should trigger a concurrency + // exception + try { + util.save(obj); + fail("Should have thrown an exception"); + } catch ( Exception expected ) { + } + } + } + + private T getUniqueObject(T obj) { + obj.setKey("" + startKey++); + obj.setNormalStringAttribute("" + startKey++); + return obj; + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/AllSupportedAnnotationsClass.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/AllSupportedAnnotationsClass.java new file mode 100644 index 000000000000..1b9c36712d33 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/AllSupportedAnnotationsClass.java @@ -0,0 +1,219 @@ +/* + * 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.mapper.dynamodb.pojos; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAttribute; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAutoGeneratedDefault; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAutoGeneratedTimestamp; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAutoGenerateStrategy; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBConvertedBool; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBDelimited; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBFlattened; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBGeneratedUuid; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBIgnore; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBIndexHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBIndexRangeKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBKeyed; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBNamed; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTypeConvertedJson; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTypeConvertedEpochDate; + +import software.amazon.awssdk.services.dynamodb.model.KeyType; + +import java.util.Date; +import java.util.UUID; + +@DynamoDBTable(tableName = "aws-java-sdk-all-supported-annotations") +public class AllSupportedAnnotationsClass extends TestDocClass { + + public static final String IGNORED_ATTRIBUTE_VALUE = "ignoredAttribute"; + + private String rangeKey; + private Integer integerAttribute; + private UUID uuid; + private Date date; + private Integer defaultAttribute; + private Boolean booleanAttribute; + private PhoneNumber phoneNumber; + private String ignoredAttribute = IGNORED_ATTRIBUTE_VALUE; + private String gsiHashKey; + private String gsiRangeKey; + private String namedAttribute; + private Currency currencyAttribute; + private Date epochDate; + private DateRange dateRange; + + @DynamoDBKeyed(KeyType.RANGE) + public String getRangeKey() { + return rangeKey; + } + + public void setRangeKey(String rangeKey) { + this.rangeKey = rangeKey; + } + + @DynamoDBAttribute(attributeName = "integerAttributeNew") + public Integer getIntegerAttribute() { + return integerAttribute; + } + + public void setIntegerAttribute(Integer integerAttribute) { + this.integerAttribute = integerAttribute; + } + + @DynamoDBGeneratedUuid(DynamoDBAutoGenerateStrategy.ALWAYS) + public UUID getUuid() { + return uuid; + } + + public void setUuid(UUID uuid) { + this.uuid = uuid; + } + + @DynamoDBAutoGeneratedTimestamp + @DynamoDBAttribute + public Date getDate() { + return date; + } + + public void setDate(Date date) { + this.date = date; + } + + @DynamoDBAutoGeneratedDefault("1") + public Integer getDefaultAttribute() { + return defaultAttribute; + } + + public void setDefaultAttribute(Integer defaultAttribute) { + this.defaultAttribute = defaultAttribute; + } + + @DynamoDBConvertedBool(DynamoDBConvertedBool.Format.Y_N) + public Boolean getBooleanAttribute() { + return booleanAttribute; + } + + public void setBooleanAttribute(Boolean booleanAttribute) { + this.booleanAttribute = booleanAttribute; + } + + @DynamoDBDelimited(attributeNames={"areaCode", "exchange", "subscriber"}, delimiter='-') + public PhoneNumber getPhoneNumber() { + return phoneNumber; + } + + public void setPhoneNumber(PhoneNumber phoneNumber) { + this.phoneNumber = phoneNumber; + } + + @DynamoDBIgnore + public String getIgnoredAttribute() { + return ignoredAttribute; + } + + public void setIgnoredAttribute(String ignoredAttribute) { + this.ignoredAttribute = ignoredAttribute; + } + + @DynamoDBIndexHashKey(attributeName="gsi_hashKey", globalSecondaryIndexName="gsi") + public String getGsiHashKey() { + return gsiHashKey; + } + + public void setGsiHashKey(String gsiHashKey) { + this.gsiHashKey = gsiHashKey; + } + + @DynamoDBIndexRangeKey(attributeName="gsi_rangeKey", globalSecondaryIndexName="gsi") + public String getGsiRangeKey() { + return this.gsiRangeKey; + } + + public void setGsiRangeKey(String gsiRangeKey) { + this.gsiRangeKey = gsiRangeKey; + } + + @DynamoDBNamed("renamedAttribute") + public String getNamedAttribute() { + return namedAttribute; + } + public void setNamedAttribute(String namedAttribute) { + this.namedAttribute = namedAttribute; + } + + @DynamoDBTypeConvertedJson + public Currency getCurrencyAttribute() { + return currencyAttribute; + } + + public void setCurrencyAttribute(Currency currencyAttribute) { + this.currencyAttribute = currencyAttribute; + } + + @DynamoDBTypeConvertedEpochDate + public Date getEpochDate() { + return epochDate; + } + + public void setEpochDate(Date epochDate) { + this.epochDate = epochDate; + } + + @DynamoDBFlattened(attributes={ + @DynamoDBAttribute(mappedBy="start", attributeName="DateRangeStart"), + @DynamoDBAttribute(mappedBy="end", attributeName="DateRangeEnd")}) + public DateRange getDateRange() { + return dateRange; + } + + public void setDateRange(final DateRange dateRange) { + this.dateRange = dateRange; + } + + @Override + public boolean equals(Object obj) { + AllSupportedAnnotationsClass other = (AllSupportedAnnotationsClass) obj; + + return (eq(rangeKey, other.rangeKey) + && eq(integerAttribute, other.integerAttribute) + && eq(uuid, other.uuid) + && eq(date, other.date) + && eq(defaultAttribute, other.defaultAttribute) + && eq(booleanAttribute, other.booleanAttribute) + && eq(phoneNumber, other.phoneNumber) + && eq(ignoredAttribute, other.ignoredAttribute) + && eq(gsiHashKey, other.gsiHashKey) + && eq(gsiRangeKey, other.gsiRangeKey) + && eq(namedAttribute, other.namedAttribute) + && eq(currencyAttribute, other.currencyAttribute) + && eq(epochDate, other.epochDate) + && eq(dateRange, other.dateRange) + && super.equals(other) + ); + } + + @Override + public String toString() { + return "{" + this.getClass().getSimpleName() + ": id=" + super.getId() + ", rangeKey=" + rangeKey + ", listOfMaps=" + super.getListOfMaps() + + ", mapOfLists=" + super.getMapOfLists() + ", integerAttribute=" + integerAttribute + ", uuid=" + uuid + ", date=" + date + + ", defaultAttribute=" + defaultAttribute + ", booleanAttribute=" + booleanAttribute + ", phoneNumber=" + phoneNumber + + ", ignoredAttribute=" + ignoredAttribute + ", gsiHashKey=" + gsiHashKey + ", gsiRangeKey=" + gsiRangeKey + + ", namedAttribute=" + namedAttribute + ", currencyAttribute=" + currencyAttribute + ", epochDate=" + epochDate +", dateRange=" + dateRange + + "}"; + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/AllSupportedDataTypesClass.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/AllSupportedDataTypesClass.java new file mode 100644 index 000000000000..c2d4d3f729e2 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/AllSupportedDataTypesClass.java @@ -0,0 +1,422 @@ +/* + * 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.mapper.dynamodb.pojos; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Calendar; +import java.util.Date; +import java.util.Set; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBRangeKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; + +/** + * Exhaustive exercise of DynamoDB domain mapping, exercising every supported + * data type. + */ +@DynamoDBTable(tableName = "aws-java-sdk-all-supported-types") +public class AllSupportedDataTypesClass { + + // key fields + private String key; + private String rangeKey; + + // simple type fields + private String stringAttribute; + private Integer integerAttribute; + private Long longAttribute; + private Double doubleAttribute; + private Float floatAttribute; + private BigDecimal bigDecimalAttribute; + private BigInteger bigIntegerAttribute; + private Byte byteAttribute; + private Date dateAttribute; + private Calendar calendarAttribute; + private Boolean booleanAttribute; + + // collection type fields + private Set stringSetAttribute; + private Set integerSetAttribute; + private Set doubleSetAttribute; + private Set floatSetAttribute; + private Set bigDecimalSetAttribute; + private Set bigIntegerSetAttribute; + private Set longSetAttribute; + private Set byteSetAttribute; + private Set dateSetAttribute; + private Set calendarSetAttribute; + private Set booleanSetAttribute; + + @DynamoDBHashKey + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + @DynamoDBRangeKey + public String getRangeKey() { + return rangeKey; + } + + public void setRangeKey(String rangeKey) { + this.rangeKey = rangeKey; + } + + public String getStringAttribute() { + return stringAttribute; + } + + public void setStringAttribute(String stringAttribute) { + this.stringAttribute = stringAttribute; + } + + public Integer getIntegerAttribute() { + return integerAttribute; + } + + public void setIntegerAttribute(Integer integerAttribute) { + this.integerAttribute = integerAttribute; + } + + public Long getLongAttribute() { + return longAttribute; + } + + public void setLongAttribute(Long longAttribute) { + this.longAttribute = longAttribute; + } + + public Double getDoubleAttribute() { + return doubleAttribute; + } + + public void setDoubleAttribute(Double doubleAttribute) { + this.doubleAttribute = doubleAttribute; + } + + public Float getFloatAttribute() { + return floatAttribute; + } + + public void setFloatAttribute(Float floatAttribute) { + this.floatAttribute = floatAttribute; + } + + public BigDecimal getBigDecimalAttribute() { + return bigDecimalAttribute; + } + + public void setBigDecimalAttribute(BigDecimal bigDecimalAttribute) { + this.bigDecimalAttribute = bigDecimalAttribute; + } + + public BigInteger getBigIntegerAttribute() { + return bigIntegerAttribute; + } + + public void setBigIntegerAttribute(BigInteger bigIntegerAttribute) { + this.bigIntegerAttribute = bigIntegerAttribute; + } + + public Byte getByteAttribute() { + return byteAttribute; + } + + public void setByteAttribute(Byte byteAttribute) { + this.byteAttribute = byteAttribute; + } + + public Date getDateAttribute() { + return dateAttribute; + } + + public void setDateAttribute(Date dateAttribute) { + this.dateAttribute = dateAttribute; + } + + public Calendar getCalendarAttribute() { + return calendarAttribute; + } + + public void setCalendarAttribute(Calendar calendarAttribute) { + this.calendarAttribute = calendarAttribute; + } + + public Boolean getBooleanAttribute() { + return booleanAttribute; + } + + public void setBooleanAttribute(Boolean booleanAttribute) { + this.booleanAttribute = booleanAttribute; + } + + public Set getIntegerSetAttribute() { + return integerSetAttribute; + } + + public void setIntegerSetAttribute(Set integerSetAttribute) { + this.integerSetAttribute = integerSetAttribute; + } + + public Set getDoubleSetAttribute() { + return doubleSetAttribute; + } + + public void setDoubleSetAttribute(Set doubleSetAttribute) { + this.doubleSetAttribute = doubleSetAttribute; + } + + public Set getFloatSetAttribute() { + return floatSetAttribute; + } + + public void setFloatSetAttribute(Set floatSetAttribute) { + this.floatSetAttribute = floatSetAttribute; + } + + public Set getBigDecimalSetAttribute() { + return bigDecimalSetAttribute; + } + + public void setBigDecimalSetAttribute(Set bigDecimalSetAttribute) { + this.bigDecimalSetAttribute = bigDecimalSetAttribute; + } + + public Set getBigIntegerSetAttribute() { + return bigIntegerSetAttribute; + } + + public void setBigIntegerSetAttribute(Set bigIntegerSetAttribute) { + this.bigIntegerSetAttribute = bigIntegerSetAttribute; + } + + public Set getLongSetAttribute() { + return longSetAttribute; + } + + public void setLongSetAttribute(Set longSetAttribute) { + this.longSetAttribute = longSetAttribute; + } + + public Set getByteSetAttribute() { + return byteSetAttribute; + } + + public void setByteSetAttribute(Set byteSetAttribute) { + this.byteSetAttribute = byteSetAttribute; + } + + public Set getDateSetAttribute() { + return dateSetAttribute; + } + + public void setDateSetAttribute(Set dateSetAttribute) { + this.dateSetAttribute = dateSetAttribute; + } + + public Set getCalendarSetAttribute() { + return calendarSetAttribute; + } + + public void setCalendarSetAttribute(Set calendarSetAttribute) { + this.calendarSetAttribute = calendarSetAttribute; + } + + public Set getBooleanSetAttribute() { + return booleanSetAttribute; + } + + public void setBooleanSetAttribute(Set booleanSetAttribute) { + this.booleanSetAttribute = booleanSetAttribute; + } + + public Set getStringSetAttribute() { + return stringSetAttribute; + } + + public void setStringSetAttribute(Set stringSetAttribute) { + this.stringSetAttribute = stringSetAttribute; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((bigDecimalAttribute == null) ? 0 : bigDecimalAttribute.hashCode()); + result = prime * result + ((bigDecimalSetAttribute == null) ? 0 : bigDecimalSetAttribute.hashCode()); + result = prime * result + ((bigIntegerAttribute == null) ? 0 : bigIntegerAttribute.hashCode()); + result = prime * result + ((bigIntegerSetAttribute == null) ? 0 : bigIntegerSetAttribute.hashCode()); + result = prime * result + ((booleanAttribute == null) ? 0 : booleanAttribute.hashCode()); + result = prime * result + ((booleanSetAttribute == null) ? 0 : booleanSetAttribute.hashCode()); + result = prime * result + ((byteAttribute == null) ? 0 : byteAttribute.hashCode()); + result = prime * result + ((byteSetAttribute == null) ? 0 : byteSetAttribute.hashCode()); + result = prime * result + ((calendarAttribute == null) ? 0 : calendarAttribute.hashCode()); + result = prime * result + ((calendarSetAttribute == null) ? 0 : calendarSetAttribute.hashCode()); + result = prime * result + ((dateAttribute == null) ? 0 : dateAttribute.hashCode()); + result = prime * result + ((dateSetAttribute == null) ? 0 : dateSetAttribute.hashCode()); + result = prime * result + ((doubleAttribute == null) ? 0 : doubleAttribute.hashCode()); + result = prime * result + ((doubleSetAttribute == null) ? 0 : doubleSetAttribute.hashCode()); + result = prime * result + ((floatAttribute == null) ? 0 : floatAttribute.hashCode()); + result = prime * result + ((floatSetAttribute == null) ? 0 : floatSetAttribute.hashCode()); + result = prime * result + ((integerAttribute == null) ? 0 : integerAttribute.hashCode()); + result = prime * result + ((integerSetAttribute == null) ? 0 : integerSetAttribute.hashCode()); + result = prime * result + ((key == null) ? 0 : key.hashCode()); + result = prime * result + ((longAttribute == null) ? 0 : longAttribute.hashCode()); + result = prime * result + ((longSetAttribute == null) ? 0 : longSetAttribute.hashCode()); + result = prime * result + ((rangeKey == null) ? 0 : rangeKey.hashCode()); + result = prime * result + ((stringAttribute == null) ? 0 : stringAttribute.hashCode()); + result = prime * result + ((stringSetAttribute == null) ? 0 : stringSetAttribute.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) + return true; + if ( obj == null ) + return false; + if ( getClass() != obj.getClass() ) + return false; + AllSupportedDataTypesClass other = (AllSupportedDataTypesClass) obj; + if ( bigDecimalAttribute == null ) { + if ( other.bigDecimalAttribute != null ) + return false; + } else if ( !bigDecimalAttribute.equals(other.bigDecimalAttribute) ) + return false; + if ( bigDecimalSetAttribute == null ) { + if ( other.bigDecimalSetAttribute != null ) + return false; + } else if ( !bigDecimalSetAttribute.equals(other.bigDecimalSetAttribute) ) + return false; + if ( bigIntegerAttribute == null ) { + if ( other.bigIntegerAttribute != null ) + return false; + } else if ( !bigIntegerAttribute.equals(other.bigIntegerAttribute) ) + return false; + if ( bigIntegerSetAttribute == null ) { + if ( other.bigIntegerSetAttribute != null ) + return false; + } else if ( !bigIntegerSetAttribute.equals(other.bigIntegerSetAttribute) ) + return false; + if ( booleanAttribute == null ) { + if ( other.booleanAttribute != null ) + return false; + } else if ( !booleanAttribute.equals(other.booleanAttribute) ) + return false; + if ( booleanSetAttribute == null ) { + if ( other.booleanSetAttribute != null ) + return false; + } else if ( !booleanSetAttribute.equals(other.booleanSetAttribute) ) + return false; + if ( byteAttribute == null ) { + if ( other.byteAttribute != null ) + return false; + } else if ( !byteAttribute.equals(other.byteAttribute) ) + return false; + if ( byteSetAttribute == null ) { + if ( other.byteSetAttribute != null ) + return false; + } else if ( !byteSetAttribute.equals(other.byteSetAttribute) ) + return false; + if ( calendarAttribute == null ) { + if ( other.calendarAttribute != null ) + return false; + } else if ( !calendarAttribute.equals(other.calendarAttribute) ) + return false; + if ( calendarSetAttribute == null ) { + if ( other.calendarSetAttribute != null ) + return false; + } else if ( !calendarSetAttribute.equals(other.calendarSetAttribute) ) + return false; + if ( dateAttribute == null ) { + if ( other.dateAttribute != null ) + return false; + } else if ( !dateAttribute.equals(other.dateAttribute) ) + return false; + if ( dateSetAttribute == null ) { + if ( other.dateSetAttribute != null ) + return false; + } else if ( !dateSetAttribute.equals(other.dateSetAttribute) ) + return false; + if ( doubleAttribute == null ) { + if ( other.doubleAttribute != null ) + return false; + } else if ( !doubleAttribute.equals(other.doubleAttribute) ) + return false; + if ( doubleSetAttribute == null ) { + if ( other.doubleSetAttribute != null ) + return false; + } else if ( !doubleSetAttribute.equals(other.doubleSetAttribute) ) + return false; + if ( floatAttribute == null ) { + if ( other.floatAttribute != null ) + return false; + } else if ( !floatAttribute.equals(other.floatAttribute) ) + return false; + if ( floatSetAttribute == null ) { + if ( other.floatSetAttribute != null ) + return false; + } else if ( !floatSetAttribute.equals(other.floatSetAttribute) ) + return false; + if ( integerAttribute == null ) { + if ( other.integerAttribute != null ) + return false; + } else if ( !integerAttribute.equals(other.integerAttribute) ) + return false; + if ( integerSetAttribute == null ) { + if ( other.integerSetAttribute != null ) + return false; + } else if ( !integerSetAttribute.equals(other.integerSetAttribute) ) + return false; + if ( key == null ) { + if ( other.key != null ) + return false; + } else if ( !key.equals(other.key) ) + return false; + if ( longAttribute == null ) { + if ( other.longAttribute != null ) + return false; + } else if ( !longAttribute.equals(other.longAttribute) ) + return false; + if ( longSetAttribute == null ) { + if ( other.longSetAttribute != null ) + return false; + } else if ( !longSetAttribute.equals(other.longSetAttribute) ) + return false; + if ( rangeKey == null ) { + if ( other.rangeKey != null ) + return false; + } else if ( !rangeKey.equals(other.rangeKey) ) + return false; + if ( stringAttribute == null ) { + if ( other.stringAttribute != null ) + return false; + } else if ( !stringAttribute.equals(other.stringAttribute) ) + return false; + if ( stringSetAttribute == null ) { + if ( other.stringSetAttribute != null ) + return false; + } else if ( !stringSetAttribute.equals(other.stringSetAttribute) ) + return false; + return true; + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/AuditRecord.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/AuditRecord.java new file mode 100644 index 000000000000..40a6aa637e0d --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/AuditRecord.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.mapper.dynamodb.pojos; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAttribute; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAutoGenerateStrategy; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAutoGeneratedTimestamp; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBFlattened; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBVersionAttribute; + +import java.util.Date; + +@DynamoDBFlattened(attributes={ + @DynamoDBAttribute(mappedBy="createdDate", attributeName="RCD"), + @DynamoDBAttribute(mappedBy="lastModifiedDate", attributeName="RMD"), + @DynamoDBAttribute(mappedBy="versionNumber", attributeName="RVN") +}) +public class AuditRecord { + + private Date createdDate; + private Date lastModifiedDate; + private Long versionNumber; + + @DynamoDBAutoGeneratedTimestamp(strategy=DynamoDBAutoGenerateStrategy.CREATE) + public Date getCreatedDate() { + return this.createdDate; + } + + public void setCreatedDate(final Date createdDate) { + this.createdDate = createdDate; + } + + @DynamoDBAutoGeneratedTimestamp(strategy=DynamoDBAutoGenerateStrategy.ALWAYS) + public Date getLastModifiedDate() { + return this.lastModifiedDate; + } + + public void setLastModifiedDate(final Date lastModifiedDate) { + this.lastModifiedDate = lastModifiedDate; + } + + @DynamoDBVersionAttribute + public Long getVersionNumber() { + return this.versionNumber; + } + + public void setVersionNumber(final Long versionNumber) { + this.versionNumber = versionNumber; + } + + @Override + public final boolean equals(final Object o) { + if (o == this) return true; + if (!(o instanceof AuditRecord)) return false; + AuditRecord that = (AuditRecord)o; + return (createdDate == null ? that.createdDate == null : createdDate.equals(that.createdDate)) && + (lastModifiedDate == null ? that.lastModifiedDate == null : lastModifiedDate.equals(that.lastModifiedDate)) && + (versionNumber == null ? that.versionNumber == null : versionNumber.equals(that.versionNumber)); + } + + @Override + public final int hashCode() { + return 1 + (createdDate == null ? 0 : createdDate.hashCode()) + + (lastModifiedDate == null ? 0 : lastModifiedDate.hashCode()) + + (versionNumber == null ? 0 : versionNumber.hashCode()); + } + + @Override + public final String toString() { + return getClass().getName() + "{" + + "createdDate=" + createdDate + "," + + "lastModifiedDate=" + lastModifiedDate + "," + + "versionNumber=" + versionNumber + + "}"; + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/AutoKeyAndVal.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/AutoKeyAndVal.java new file mode 100644 index 000000000000..77efcae11bc8 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/AutoKeyAndVal.java @@ -0,0 +1,44 @@ +/* + * 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.mapper.dynamodb.pojos; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAutoGeneratedKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; + +/** + * Auto-generated string key and value. + */ +@DynamoDBTable(tableName="aws-java-sdk-util") +public class AutoKeyAndVal extends KeyAndVal { + @Override @DynamoDBHashKey + @DynamoDBAutoGeneratedKey + public String getKey() { + return super.getKey(); + } + @Override + public void setKey(final String key) { + super.setKey(key); + } + @Override + public V getVal() { + return super.getVal(); + } + @Override + public void setVal(final V val) { + super.setVal(val); + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/BinaryAttributeByteArrayClass.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/BinaryAttributeByteArrayClass.java new file mode 100644 index 000000000000..b25a76daf034 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/BinaryAttributeByteArrayClass.java @@ -0,0 +1,98 @@ +/* + * 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.mapper.dynamodb.pojos; + +import java.util.Set; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAttribute; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; + +/** + * Test domain class with byte[] attribute, byte[] set and a string key + */ +@DynamoDBTable(tableName = "aws-java-sdk-util") +public class BinaryAttributeByteArrayClass { + + private String key; + private byte[] binaryAttribute; + private Set binarySetAttribute; + + @DynamoDBHashKey(attributeName = "key") + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + @DynamoDBAttribute(attributeName = "binaryAttribute") + public byte[] getBinaryAttribute() { + return binaryAttribute; + } + + public void setBinaryAttribute(byte[] binaryAttribute) { + this.binaryAttribute = binaryAttribute; + } + + @DynamoDBAttribute(attributeName = "binarySetAttribute") + public Set getBinarySetAttribute() { + return binarySetAttribute; + } + + public void setBinarySetAttribute(Set binarySetAttribute) { + this.binarySetAttribute = binarySetAttribute; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((key == null) ? 0 : key.hashCode()); + result = prime * result + ((binaryAttribute == null) ? 0 : binaryAttribute.hashCode()); + result = prime * result + ((binarySetAttribute == null) ? 0 : binarySetAttribute.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) + return true; + if ( obj == null ) + return false; + if ( getClass() != obj.getClass() ) + return false; + BinaryAttributeByteArrayClass other = (BinaryAttributeByteArrayClass) obj; + if ( key == null ) { + if ( other.key != null ) + return false; + } else if ( !key.equals(other.key) ) + return false; + if ( binaryAttribute == null ) { + if ( other.binaryAttribute != null ) + return false; + } else if ( !binaryAttribute.equals(other.binaryAttribute) ) + return false; + if ( binarySetAttribute == null ) { + if ( other.binarySetAttribute != null ) + return false; + } else if ( !binarySetAttribute.equals(other.binarySetAttribute) ) + return false; + return true; + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/BinaryAttributeByteBufferClass.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/BinaryAttributeByteBufferClass.java new file mode 100644 index 000000000000..20057728da7c --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/BinaryAttributeByteBufferClass.java @@ -0,0 +1,99 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.mapper.dynamodb.pojos; + +import java.nio.ByteBuffer; +import java.util.Set; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAttribute; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; + +/** + * Test domain class with byteBuffer attribute, byteBuffer set and a string key + */ +@DynamoDBTable(tableName = "aws-java-sdk-util") +public class BinaryAttributeByteBufferClass { + + private String key; + private ByteBuffer binaryAttribute; + private Set binarySetAttribute; + + @DynamoDBHashKey(attributeName = "key") + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + @DynamoDBAttribute(attributeName = "binaryAttribute") + public ByteBuffer getBinaryAttribute() { + return binaryAttribute; + } + + public void setBinaryAttribute(ByteBuffer binaryAttribute) { + this.binaryAttribute = binaryAttribute; + } + + @DynamoDBAttribute(attributeName = "binarySetAttribute") + public Set getBinarySetAttribute() { + return binarySetAttribute; + } + + public void setBinarySetAttribute(Set binarySetAttribute) { + this.binarySetAttribute = binarySetAttribute; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((key == null) ? 0 : key.hashCode()); + result = prime * result + ((binaryAttribute == null) ? 0 : binaryAttribute.hashCode()); + result = prime * result + ((binarySetAttribute == null) ? 0 : binarySetAttribute.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) + return true; + if ( obj == null ) + return false; + if ( getClass() != obj.getClass() ) + return false; + BinaryAttributeByteBufferClass other = (BinaryAttributeByteBufferClass) obj; + if ( key == null ) { + if ( other.key != null ) + return false; + } else if ( !key.equals(other.key) ) + return false; + if ( binaryAttribute == null ) { + if ( other.binaryAttribute != null ) + return false; + } else if ( !binaryAttribute.equals(other.binaryAttribute) ) + return false; + if ( binarySetAttribute == null ) { + if ( other.binarySetAttribute != null ) + return false; + } else if ( !binarySetAttribute.equals(other.binarySetAttribute) ) + return false; + return true; + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/Currency.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/Currency.java new file mode 100644 index 000000000000..d2d542039bb4 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/Currency.java @@ -0,0 +1,53 @@ +/* + * 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.mapper.dynamodb.pojos; + +/** + * A currency object. + */ +public class Currency { + private KeyAndVal kav; + public Currency(final Double amount, final String unit) { + this.kav = new KeyAndVal(amount, unit); + } + public Currency() { + this((Double)null, (String)null); + } + public Double getAmount() { + return kav.getKey(); + } + public void setAmount(final Double amount) { + kav.setKey(amount); + } + public String getUnit() { + return kav.getVal(); + } + public void setUnit(final String unit) { + kav.setVal(unit); + } + @Override + public final boolean equals(final Object o) { + return (o == this || (o instanceof Currency && kav.equals(((Currency)o).kav))); + } + @Override + public final int hashCode() { + return kav.hashCode(); + } + @Override + public final String toString() { + return kav.toString(); + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/CustomBooleanClass.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/CustomBooleanClass.java new file mode 100644 index 000000000000..0b375abbb95c --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/CustomBooleanClass.java @@ -0,0 +1,111 @@ +/* + * 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.mapper.dynamodb.pojos; + +import static software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperFieldModel.DynamoDBAttributeType; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAttribute; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTyped; + +@DynamoDBTable(tableName = "custom-boolean-class") +public class CustomBooleanClass { + private String key; + private CustomBoolean customBooleanAttribute; + + @DynamoDBHashKey + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + @DynamoDBTyped(DynamoDBAttributeType.S) + @DynamoDBAttribute + public CustomBoolean getCustomBooleanAttribute() { + return customBooleanAttribute; + } + + public void setCustomBooleanAttribute(CustomBoolean customBooleanAttribute) { + this.customBooleanAttribute = customBooleanAttribute; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((key == null) ? 0 : key.hashCode()); + result = prime * result + ((customBooleanAttribute == null) ? 0 : customBooleanAttribute.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) + return true; + if ( obj == null ) + return false; + if ( getClass() != obj.getClass() ) + return false; + CustomBooleanClass other = (CustomBooleanClass) obj; + if ( key == null ) { + if ( other.key != null ) + return false; + } else if ( !key.equals(other.key) ) + return false; + if ( customBooleanAttribute == null ) { + if ( other.customBooleanAttribute != null ) + return false; + } else if ( !customBooleanAttribute.equals(other.customBooleanAttribute) ) + return false; + return true; + } + + public static class CustomBoolean { + private final boolean value; + + public CustomBoolean(boolean value) { + this.value = value; + } + + public boolean getValue() { + return value; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (value ? 1231 : 1237); + return result; + } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) + return true; + if ( obj == null ) + return false; + if ( getClass() != obj.getClass() ) + return false; + CustomBoolean other = (CustomBoolean) obj; + return value == other.value; + } + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/DateRange.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/DateRange.java new file mode 100644 index 000000000000..ffbfd40df3de --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/DateRange.java @@ -0,0 +1,58 @@ +/* + * 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.mapper.dynamodb.pojos; + +import java.util.Date; + +/** + * A date range object. + */ +public class DateRange { + private KeyAndVal kav; + public DateRange(final Date start, final Date end) { + this.kav = new KeyAndVal(start, end); + } + public DateRange(final Date date, final Long start, final Long end) { + this(start == null ? null : new Date(date.getTime() + start), end == null ? null : new Date(date.getTime() + end)); + } + public DateRange() { + this((Date)null, (Date)null); + } + public Date getStart() { + return kav.getKey(); + } + public void setStart(final Date start) { + kav.setKey(start); + } + public Date getEnd() { + return kav.getVal(); + } + public void setEnd(final Date end) { + kav.setVal(end); + } + @Override + public final boolean equals(final Object o) { + return (o == this || (o instanceof DateRange && kav.equals(((DateRange)o).kav))); + } + @Override + public final int hashCode() { + return kav.hashCode(); + } + @Override + public final String toString() { + return kav.toString(); + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/GsiWithAlwaysUpdateTimestamp.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/GsiWithAlwaysUpdateTimestamp.java new file mode 100644 index 000000000000..82591fb57baa --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/GsiWithAlwaysUpdateTimestamp.java @@ -0,0 +1,66 @@ +/* + * 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.mapper.dynamodb.pojos; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAutoGenerateStrategy; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAutoGeneratedTimestamp; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBIndexHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBIndexRangeKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBRangeKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; + +@DynamoDBTable(tableName = "Foo") +public class GsiWithAlwaysUpdateTimestamp { + + @DynamoDBHashKey + private String hashKey; + + @DynamoDBRangeKey + private String rangeKey; + + @DynamoDBAutoGeneratedTimestamp(strategy = DynamoDBAutoGenerateStrategy.ALWAYS) + @DynamoDBIndexHashKey(globalSecondaryIndexName = "last-mod-date") + private Long lastModifiedDate; + + public String getHashKey() { + return hashKey; + } + + public GsiWithAlwaysUpdateTimestamp setHashKey(String hashKey) { + this.hashKey = hashKey; + return this; + } + + public String getRangeKey() { + return rangeKey; + } + + public GsiWithAlwaysUpdateTimestamp setRangeKey(String rangeKey) { + this.rangeKey = rangeKey; + return this; + } + + public Long getLastModifiedDate() { + return lastModifiedDate; + } + + public GsiWithAlwaysUpdateTimestamp setLastModifiedDate(Long lastModifiedDate) { + this.lastModifiedDate = lastModifiedDate; + return this; + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/HashKeyAutoGenerated.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/HashKeyAutoGenerated.java new file mode 100644 index 000000000000..1ce66cda1df6 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/HashKeyAutoGenerated.java @@ -0,0 +1,93 @@ +/* + * 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.mapper.dynamodb.pojos; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAutoGeneratedKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBRangeKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; + +@DynamoDBTable(tableName = "aws-java-sdk-string-range") +public class HashKeyAutoGenerated { + + private String key; + private String rangeKey; + private String otherAttribute; + + @DynamoDBAutoGeneratedKey + @DynamoDBHashKey + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + @DynamoDBRangeKey + public String getRangeKey() { + return rangeKey; + } + + public void setRangeKey(String rangeKey) { + this.rangeKey = rangeKey; + } + + public String getOtherAttribute() { + return otherAttribute; + } + + public void setOtherAttribute(String otherAttribute) { + this.otherAttribute = otherAttribute; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((key == null) ? 0 : key.hashCode()); + result = prime * result + ((otherAttribute == null) ? 0 : otherAttribute.hashCode()); + result = prime * result + ((rangeKey == null) ? 0 : rangeKey.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) + return true; + if ( obj == null ) + return false; + if ( getClass() != obj.getClass() ) + return false; + HashKeyAutoGenerated other = (HashKeyAutoGenerated) obj; + if ( key == null ) { + if ( other.key != null ) + return false; + } else if ( !key.equals(other.key) ) + return false; + if ( otherAttribute == null ) { + if ( other.otherAttribute != null ) + return false; + } else if ( !otherAttribute.equals(other.otherAttribute) ) + return false; + if ( rangeKey == null ) { + if ( other.rangeKey != null ) + return false; + } else if ( !rangeKey.equals(other.rangeKey) ) + return false; + return true; + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/HashKeyRangeKeyBothAutoGenerated.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/HashKeyRangeKeyBothAutoGenerated.java new file mode 100644 index 000000000000..10b095f110a4 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/HashKeyRangeKeyBothAutoGenerated.java @@ -0,0 +1,94 @@ +/* + * 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.mapper.dynamodb.pojos; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAutoGeneratedKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBRangeKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; + +@DynamoDBTable(tableName = "aws-java-sdk-string-range") +public class HashKeyRangeKeyBothAutoGenerated { + + private String key; + private String rangeKey; + private String otherAttribute; + + @DynamoDBAutoGeneratedKey + @DynamoDBHashKey + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + @DynamoDBAutoGeneratedKey + @DynamoDBRangeKey + public String getRangeKey() { + return rangeKey; + } + + public void setRangeKey(String rangeKey) { + this.rangeKey = rangeKey; + } + + public String getOtherAttribute() { + return otherAttribute; + } + + public void setOtherAttribute(String otherAttribute) { + this.otherAttribute = otherAttribute; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((key == null) ? 0 : key.hashCode()); + result = prime * result + ((otherAttribute == null) ? 0 : otherAttribute.hashCode()); + result = prime * result + ((rangeKey == null) ? 0 : rangeKey.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) + return true; + if ( obj == null ) + return false; + if ( getClass() != obj.getClass() ) + return false; + HashKeyRangeKeyBothAutoGenerated other = (HashKeyRangeKeyBothAutoGenerated) obj; + if ( key == null ) { + if ( other.key != null ) + return false; + } else if ( !key.equals(other.key) ) + return false; + if ( otherAttribute == null ) { + if ( other.otherAttribute != null ) + return false; + } else if ( !otherAttribute.equals(other.otherAttribute) ) + return false; + if ( rangeKey == null ) { + if ( other.rangeKey != null ) + return false; + } else if ( !rangeKey.equals(other.rangeKey) ) + return false; + return true; + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/HashKeyRangeKeyClass.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/HashKeyRangeKeyClass.java new file mode 100644 index 000000000000..430cf334f2cd --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/HashKeyRangeKeyClass.java @@ -0,0 +1,96 @@ +/* + * 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.mapper.dynamodb.pojos; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAttribute; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBRangeKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; + +@DynamoDBTable(tableName = "aws-java-sdk-hash-range-test") +public class HashKeyRangeKeyClass { + + private long hashKey; + private double rangeKey; + + private String stringAttribute; + + @DynamoDBHashKey + public long getHashKey() { + return hashKey; + } + + public void setHashKey(long hashKey) { + this.hashKey = hashKey; + } + + @DynamoDBRangeKey + public double getRangeKey() { + return rangeKey; + } + + public void setRangeKey(double rangeKey) { + this.rangeKey = rangeKey; + } + + @DynamoDBAttribute + public String getStringAttribute() { + return stringAttribute; + } + + public void setStringAttribute(String stringAttribute) { + this.stringAttribute = stringAttribute; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (int) (hashKey ^ (hashKey >>> 32)); + long temp; + temp = Double.doubleToLongBits(rangeKey); + result = prime * result + (int) (temp ^ (temp >>> 32)); + result = prime * result + ((stringAttribute == null) ? 0 : stringAttribute.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) + return true; + if ( obj == null ) + return false; + if ( getClass() != obj.getClass() ) + return false; + HashKeyRangeKeyClass other = (HashKeyRangeKeyClass) obj; + if ( hashKey != other.hashKey ) + return false; + if ( Double.doubleToLongBits(rangeKey) != Double.doubleToLongBits(other.rangeKey) ) + return false; + if ( stringAttribute == null ) { + if ( other.stringAttribute != null ) + return false; + } else if ( !stringAttribute.equals(other.stringAttribute) ) + return false; + return true; + } + + @Override + public String toString() { + return "HashKeyRangeKeyClass [hashKey=" + hashKey + ", rangeKey=" + rangeKey + ", stringAttribute=" + stringAttribute + "]"; + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/HashRangeAndOtherAttributeAutoGen.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/HashRangeAndOtherAttributeAutoGen.java new file mode 100644 index 000000000000..61798c62094d --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/HashRangeAndOtherAttributeAutoGen.java @@ -0,0 +1,106 @@ +/* + * 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.mapper.dynamodb.pojos; + +import java.util.Date; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAttribute; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAutoGeneratedKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAutoGeneratedTimestamp; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBRangeKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; + +@DynamoDBTable(tableName = "aws-java-sdk-hash-range-test") +public class HashRangeAndOtherAttributeAutoGen { + + private String key; + private String rangeKey; + + private Date date; + + @DynamoDBAutoGeneratedKey + @DynamoDBHashKey + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + @DynamoDBAutoGeneratedKey + @DynamoDBRangeKey + public String getRangeKey() { + return rangeKey; + } + + public void setRangeKey(String rangeKey) { + this.rangeKey = rangeKey; + } + + @DynamoDBAutoGeneratedTimestamp + @DynamoDBAttribute + public Date getDate() { + return date; + } + + public void setDate(Date date) { + this.date = date; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((key == null) ? 0 : key.hashCode()); + result = prime * result + ((rangeKey == null) ? 0 : rangeKey.hashCode()); + result = prime * result + ((date == null) ? 0 : date.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) + return true; + if ( obj == null ) + return false; + if ( getClass() != obj.getClass() ) + return false; + HashRangeAndOtherAttributeAutoGen other = (HashRangeAndOtherAttributeAutoGen) obj; + if ( key == null ) { + if ( other.key != null ) + return false; + } else if ( !key.equals(other.key) ) + return false; + if ( date == null ) { + if ( other.date != null ) + return false; + } else if ( !date.equals(other.date) ) + return false; + if ( rangeKey == null ) { + if ( other.rangeKey != null ) + return false; + } else if ( !rangeKey.equals(other.rangeKey) ) + return false; + return true; + } + + @Override + public String toString() { + return "HashRangeAndOtherAttributeAutoGen [hashKey=" + key + ", rangeKey=" + rangeKey + ", date=" + date + "]"; + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/KeyAndVal.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/KeyAndVal.java new file mode 100644 index 000000000000..cd118390bc20 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/KeyAndVal.java @@ -0,0 +1,61 @@ +/* + * 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.mapper.dynamodb.pojos; + +/** + * Simple key-val domain class. + */ +public class KeyAndVal { + private K key; + private V val; + public KeyAndVal(final K key, final V val) { + this.key = key; + this.val = val; + } + public KeyAndVal() { + this((K)null, (V)null); + } + public K getKey() { + return this.key; + } + public void setKey(final K key) { + this.key = key; + } + public V getVal() { + return this.val; + } + public void setVal(final V val) { + this.val = val; + } + public final boolean keyEquals(final Object o) { + return key == o || (key != null && key.equals(o)); + } + public final boolean valEquals(final Object o) { + return val == o || (val != null && val.equals(o)); + } + @Override + public final boolean equals(final Object o) { + return o == this || (o instanceof KeyAndVal && keyEquals(((KeyAndVal)o).key) && valEquals(((KeyAndVal)o).val)); + } + @Override + public final int hashCode() { + return 1 + (key == null ? 0 : key.hashCode()) + (val == null ? 0 : val.hashCode()); + } + @Override + public final String toString() { + return "[" + key + "," + val + "]"; + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/MultiVersionRangeKeyClass.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/MultiVersionRangeKeyClass.java new file mode 100644 index 000000000000..104b8f62337f --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/MultiVersionRangeKeyClass.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.mapper.dynamodb.pojos; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBVersionAttribute; + +public class MultiVersionRangeKeyClass extends RangeKeyClass { + + private Long version2; + + @DynamoDBVersionAttribute + public Long getVersion2() { + return version2; + } + + public void setVersion2(Long version2) { + this.version2 = version2; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((version2 == null) ? 0 : version2.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) + return true; + if ( !super.equals(obj) ) + return false; + if ( getClass() != obj.getClass() ) + return false; + MultiVersionRangeKeyClass other = (MultiVersionRangeKeyClass) obj; + if ( version2 == null ) { + if ( other.version2 != null ) + return false; + } else if ( !version2.equals(other.version2) ) + return false; + return true; + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/PhoneNumber.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/PhoneNumber.java new file mode 100644 index 000000000000..e084017d824d --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/PhoneNumber.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.mapper.dynamodb.pojos; + +/** + * A phone number object. + */ +public class PhoneNumber { + + private String areaCode; + private String exchange; + private String subscriber; + + public PhoneNumber(final String areaCode, final String exchange, final String subscriber) { + setAreaCode(areaCode); + setExchange(exchange); + setSubscriber(subscriber); + } + + public PhoneNumber() { + this(null, null, null); + } + + public String getAreaCode() { + return this.areaCode; + } + + public void setAreaCode(final String areaCode) { + this.areaCode = areaCode; + } + + public String getExchange() { + return this.exchange; + } + + public void setExchange(final String exchange) { + this.exchange = exchange; + } + + public String getSubscriber() { + return this.subscriber; + } + + public void setSubscriber(String subscriber) { + this.subscriber = subscriber; + } + + public final boolean areaCodeEquals(final Object o) { + return (areaCode == o || (areaCode != null && areaCode.equals(o))); + } + + public final boolean exchangeEquals(final Object o) { + return (exchange == o || (exchange != null && exchange.equals(o))); + } + + public final boolean subscriberEquals(final Object o) { + return (subscriber == o || (subscriber != null && subscriber.equals(o))); + } + + @Override + public final boolean equals(final Object o) { + return (o == this || (o instanceof PhoneNumber && areaCodeEquals(((PhoneNumber)o).areaCode) && exchangeEquals(((PhoneNumber)o).exchange) && subscriberEquals(((PhoneNumber)o).subscriber))); + } + + @Override + public final int hashCode() { + return (1 + (areaCode == null ? 0 : areaCode.hashCode()) + (exchange == null ? 0 : exchange.hashCode()) + (subscriber == null ? 0 : subscriber.hashCode())); + } + + @Override + public final String toString() { + return (getClass().getName() + "{areaCode=" + areaCode + ",exchange=" + exchange + ",subscriber=" + subscriber + "}"); + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/PrimaryKeysNotAutogenerated_IndexKeysAutogenerated.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/PrimaryKeysNotAutogenerated_IndexKeysAutogenerated.java new file mode 100644 index 000000000000..6b6b3c26d22f --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/PrimaryKeysNotAutogenerated_IndexKeysAutogenerated.java @@ -0,0 +1,91 @@ +/* + * 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.mapper.dynamodb.pojos; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAutoGeneratedKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBIndexHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBIndexRangeKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBRangeKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; + +@DynamoDBTable(tableName = "aws-java-sdk-string-range") +public class PrimaryKeysNotAutogenerated_IndexKeysAutogenerated { + + private String key; + private String rangeKey; + private String gsiHashKey; + private String gsiRangeKey; + + @DynamoDBHashKey + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + @DynamoDBRangeKey + public String getRangeKey() { + return rangeKey; + } + + public void setRangeKey(String rangeKey) { + this.rangeKey = rangeKey; + } + + @DynamoDBIndexHashKey(globalSecondaryIndexName = "gsi-with-autogenerated-keys", attributeName = "gis-hash-key") + @DynamoDBAutoGeneratedKey + public String getGsiHashKey() { + return gsiHashKey; + } + + public void setGsiHashKey(String gsiHashKey) { + this.gsiHashKey = gsiHashKey; + } + + @DynamoDBIndexRangeKey(globalSecondaryIndexName = "gsi-with-autogenerated-keys", attributeName = "gis-range-key") + @DynamoDBAutoGeneratedKey + public String getGsiRangeKey() { + return gsiRangeKey; + } + + public void setGsiRangeKey(String gsiRangeKey) { + this.gsiRangeKey = gsiRangeKey; + } + + @Override + public boolean equals(Object object) { + if ( !(object instanceof PrimaryKeysNotAutogenerated_IndexKeysAutogenerated)) { + return false; + } + PrimaryKeysNotAutogenerated_IndexKeysAutogenerated other = (PrimaryKeysNotAutogenerated_IndexKeysAutogenerated) object; + + return isEqual(this.getKey(), other.getKey()) + && isEqual(this.getRangeKey(), other.getRangeKey()) + && isEqual(this.getGsiHashKey(), other.getGsiHashKey()) + && isEqual(this.getGsiRangeKey(), other.getGsiRangeKey()); + } + + private static boolean isEqual(Object a, Object b) { + if (a == null || b == null) { + return a == null && b == null; + } + return a.equals(b); + } + +} \ No newline at end of file diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/PrimaryKeysNotAutogenerated_OtherAttributeAutogenerated.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/PrimaryKeysNotAutogenerated_OtherAttributeAutogenerated.java new file mode 100644 index 000000000000..0f9308e3164a --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/PrimaryKeysNotAutogenerated_OtherAttributeAutogenerated.java @@ -0,0 +1,97 @@ +/* + * 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.mapper.dynamodb.pojos; + +import java.util.Date; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAutoGenerateStrategy; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAutoGenerated; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAutoGeneratedTimestamp; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBRangeKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; + +@DynamoDBTable(tableName = "aws-java-sdk-string-range") +public class PrimaryKeysNotAutogenerated_OtherAttributeAutogenerated { + + private String key; + private String rangeKey; + private Date otherAttribute; + + @DynamoDBHashKey + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + @DynamoDBRangeKey + public String getRangeKey() { + return rangeKey; + } + + public void setRangeKey(String rangeKey) { + this.rangeKey = rangeKey; + } + + @DynamoDBAutoGeneratedTimestamp + public Date getOtherAttribute() { + return otherAttribute; + } + + public void setOtherAttribute(Date otherAttribute) { + this.otherAttribute = otherAttribute; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((key == null) ? 0 : key.hashCode()); + result = prime * result + ((otherAttribute == null) ? 0 : otherAttribute.hashCode()); + result = prime * result + ((rangeKey == null) ? 0 : rangeKey.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) + return true; + if ( obj == null ) + return false; + if ( getClass() != obj.getClass() ) + return false; + PrimaryKeysNotAutogenerated_OtherAttributeAutogenerated other = (PrimaryKeysNotAutogenerated_OtherAttributeAutogenerated) obj; + if ( key == null ) { + if ( other.key != null ) + return false; + } else if ( !key.equals(other.key) ) + return false; + if ( otherAttribute == null ) { + if ( other.otherAttribute != null ) + return false; + } else if ( !otherAttribute.equals(other.otherAttribute) ) + return false; + if ( rangeKey == null ) { + if ( other.rangeKey != null ) + return false; + } else if ( !rangeKey.equals(other.rangeKey) ) + return false; + return true; + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/RangeKeyClass.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/RangeKeyClass.java new file mode 100644 index 000000000000..067fb8a4f33f --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/RangeKeyClass.java @@ -0,0 +1,169 @@ +/* + * 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.mapper.dynamodb.pojos; + +import java.math.BigDecimal; +import java.util.Set; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAttribute; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBRangeKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBVersionAttribute; + +/** + * Comprehensive domain class + */ +@DynamoDBTable(tableName = "aws-java-sdk-range-test") +public class RangeKeyClass { + + private long key; + private double rangeKey; + private Long version; + + private Set integerSetAttribute; + private Set stringSetAttribute; + private BigDecimal bigDecimalAttribute; + private String stringAttribute; + + @DynamoDBHashKey + public long getKey() { + return key; + } + + public void setKey(long key) { + this.key = key; + } + + @DynamoDBRangeKey + public double getRangeKey() { + return rangeKey; + } + + public void setRangeKey(double rangeKey) { + this.rangeKey = rangeKey; + } + + @DynamoDBAttribute(attributeName = "integerSetAttribute") + public Set getIntegerAttribute() { + return integerSetAttribute; + } + + public void setIntegerAttribute(Set integerAttribute) { + this.integerSetAttribute = integerAttribute; + } + + @DynamoDBAttribute + public Set getStringSetAttribute() { + return stringSetAttribute; + } + + public void setStringSetAttribute(Set stringSetAttribute) { + this.stringSetAttribute = stringSetAttribute; + } + + @DynamoDBAttribute + public BigDecimal getBigDecimalAttribute() { + return bigDecimalAttribute; + } + + public void setBigDecimalAttribute(BigDecimal bigDecimalAttribute) { + this.bigDecimalAttribute = bigDecimalAttribute; + } + + @DynamoDBAttribute + public String getStringAttribute() { + return stringAttribute; + } + + public void setStringAttribute(String stringAttribute) { + this.stringAttribute = stringAttribute; + } + + @DynamoDBVersionAttribute + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((bigDecimalAttribute == null) ? 0 : bigDecimalAttribute.hashCode()); + result = prime * result + ((integerSetAttribute == null) ? 0 : integerSetAttribute.hashCode()); + result = prime * result + (int) (key ^ (key >>> 32)); + long temp; + temp = Double.doubleToLongBits(rangeKey); + result = prime * result + (int) (temp ^ (temp >>> 32)); + result = prime * result + ((stringAttribute == null) ? 0 : stringAttribute.hashCode()); + result = prime * result + ((stringSetAttribute == null) ? 0 : stringSetAttribute.hashCode()); + result = prime * result + ((version == null) ? 0 : version.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) + return true; + if ( obj == null ) + return false; + if ( getClass() != obj.getClass() ) + return false; + RangeKeyClass other = (RangeKeyClass) obj; + if ( bigDecimalAttribute == null ) { + if ( other.bigDecimalAttribute != null ) + return false; + } else if ( !bigDecimalAttribute.equals(other.bigDecimalAttribute) ) + return false; + if ( integerSetAttribute == null ) { + if ( other.integerSetAttribute != null ) + return false; + } else if ( !integerSetAttribute.equals(other.integerSetAttribute) ) + return false; + if ( key != other.key ) + return false; + if ( Double.doubleToLongBits(rangeKey) != Double.doubleToLongBits(other.rangeKey) ) + return false; + if ( stringAttribute == null ) { + if ( other.stringAttribute != null ) + return false; + } else if ( !stringAttribute.equals(other.stringAttribute) ) + return false; + if ( stringSetAttribute == null ) { + if ( other.stringSetAttribute != null ) + return false; + } else if ( !stringSetAttribute.equals(other.stringSetAttribute) ) + return false; + if ( version == null ) { + if ( other.version != null ) + return false; + } else if ( !version.equals(other.version) ) + return false; + return true; + } + + @Override + public String toString() { + return "RangeKeyClass [key=" + key + ", rangeKey=" + rangeKey + ", version=" + version + + ", integerSetAttribute=" + integerSetAttribute + ", stringSetAttribute=" + stringSetAttribute + + ", bigDecimalAttribute=" + bigDecimalAttribute + ", stringAttribute=" + stringAttribute + "]"; + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/S3LinksTestClass.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/S3LinksTestClass.java new file mode 100644 index 000000000000..912367276357 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/S3LinksTestClass.java @@ -0,0 +1,61 @@ +/* + * 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.mapper.dynamodb.pojos; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; +import software.amazon.awssdk.mapper.dynamodb.S3Link; + +/** + * Test domain class with a single string key, and two S3Links + */ +@DynamoDBTable(tableName = "aws-java-sdk-util") +public class S3LinksTestClass { + + private String key; + private S3Link s3LinkWest; + private S3Link s3LinkEast; + + @DynamoDBHashKey + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public S3LinksTestClass withKey(String key) { + setKey(key); + return this; + } + + public S3Link getS3LinkWest() { + return s3LinkWest; + } + + public void setS3LinkWest(S3Link s3LinkAttribute) { + this.s3LinkWest = s3LinkAttribute; + } + + public S3Link getS3LinkEast() { + return s3LinkEast; + } + + public void setS3LinkEast(S3Link s3LinkEast) { + this.s3LinkEast = s3LinkEast; + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/SchemaViolatingTestItem.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/SchemaViolatingTestItem.java new file mode 100644 index 000000000000..0551337f67c6 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/SchemaViolatingTestItem.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.mapper.dynamodb.pojos; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBRangeKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; + +/** + * This class has same name has TestItem but different hashKeyName. This will + * help test schema violations + */ +@DynamoDBTable(tableName = "aws-java-sdk-dynamodb-mapper-save-config-test") +public class SchemaViolatingTestItem { + + private String hashKey; + private Long rangeKey; + + @DynamoDBHashKey(attributeName = "fakeHashKey") + public String getHashKey() { + return hashKey; + } + + public void setHashKey(String hashKey) { + this.hashKey = hashKey; + } + + @DynamoDBRangeKey(attributeName = "rangeKey") + public Long getRangeKey() { + return rangeKey; + } + + public void setRangeKey(Long rangeKey) { + this.rangeKey = rangeKey; + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/SlimAttributeNamesClass.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/SlimAttributeNamesClass.java new file mode 100644 index 000000000000..ced845bf7dbf --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/SlimAttributeNamesClass.java @@ -0,0 +1,181 @@ +/* + * 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.mapper.dynamodb.pojos; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAttribute; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBRangeKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; + +@DynamoDBTable(tableName = "aws-java-sdk-dynamodb-mapper-save-config-test") +public class SlimAttributeNamesClass { + private String hashKey; + private Long rangeKey; + private String z; + private String a; + private String l; + private String c; + private String g; + private String m; + + + @DynamoDBHashKey(attributeName = "hashKey") + public String getHashKey() { + return hashKey; + } + + public void setHashKey(String hashKey) { + this.hashKey = hashKey; + } + + @DynamoDBRangeKey(attributeName = "rangeKey") + public Long getRangeKey() { + return rangeKey; + } + + public void setRangeKey(Long rangeKey) { + this.rangeKey = rangeKey; + } + + @DynamoDBAttribute + public String getZ() { + return z; + } + + public void setZ(String z) { + this.z = z; + } + + @DynamoDBAttribute + public String getA() { + return a; + } + + public void setA(String a) { + this.a = a; + } + + @DynamoDBAttribute + public String getL() { + return l; + } + + public void setL(String l) { + this.l = l; + } + + @DynamoDBAttribute + public String getC() { + return c; + } + + public void setC(String c) { + this.c = c; + } + + @DynamoDBAttribute + public String getG() { + return g; + } + + public void setG(String g) { + this.g = g; + } + + @DynamoDBAttribute + public String getM() { + return m; + } + + public void setM(String a) { + this.m = m; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((hashKey == null) ? 0 : hashKey.hashCode()); + result = prime * result + ((rangeKey == null) ? 0 : rangeKey.hashCode()); + result = prime * result + ((z == null) ? 0 : z.hashCode()); + result = prime * result + ((a == null) ? 0 : a.hashCode()); + result = prime * result + ((l == null) ? 0 : l.hashCode()); + result = prime * result + ((c == null) ? 0 : c.hashCode()); + result = prime * result + ((g == null) ? 0 : g.hashCode()); + result = prime * result + ((m == null) ? 0 : m.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) + return true; + if ( obj == null ) + return false; + if ( getClass() != obj.getClass() ) + return false; + SlimAttributeNamesClass other = (SlimAttributeNamesClass) obj; + if ( hashKey == null ) { + if ( other.hashKey != null ) + return false; + } else if ( !hashKey.equals(other.hashKey) ) + return false; + if ( rangeKey == null ) { + if ( other.rangeKey != null ) + return false; + } else if ( !rangeKey.equals(other.rangeKey) ) + return false; + if ( z == null ) { + if ( other.z != null ) + return false; + } else if ( !z.equals(other.z) ) + return false; + if ( a == null ) { + if ( other.a != null ) + return false; + } else if ( !a.equals(other.a) ) + return false; + if ( l == null ) { + if ( other.l != null ) + return false; + } else if ( !l.equals(other.l) ) + return false; + if ( c == null ) { + if ( other.c != null ) + return false; + } else if ( !c.equals(other.c) ) + return false; + if ( g == null ) { + if ( other.g != null ) + return false; + } else if ( !g.equals(other.g) ) + return false; + if ( m == null ) { + if ( other.m != null ) + return false; + } else if ( !m.equals(other.m) ) + return false; + return true; + } + + @Override + public String toString() { + return "{SlimAttributeNamesClass: hashKey=" + hashKey + ", rangeKey=" + rangeKey + + ", z=" + z + ", a=" + a + ", l=" + l + ", c=" + c + ", g=" + g + ", m=" + m + "}"; + } + + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/StringAttributeClass.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/StringAttributeClass.java new file mode 100644 index 000000000000..b1c89c6574b3 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/StringAttributeClass.java @@ -0,0 +1,96 @@ +/* + * 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.mapper.dynamodb.pojos; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAttribute; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; + +/** + * Test domain class with a single string attribute and a string key + */ +@DynamoDBTable(tableName = "aws-java-sdk-util") +public class StringAttributeClass { + + private String key; + private String stringAttribute; + private String renamedAttribute; + + @DynamoDBHashKey + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + @DynamoDBAttribute + public String getStringAttribute() { + return stringAttribute; + } + + public void setStringAttribute(String stringAttribute) { + this.stringAttribute = stringAttribute; + } + + @DynamoDBAttribute(attributeName = "originalName") + public String getRenamedAttribute() { + return renamedAttribute; + } + + public void setRenamedAttribute(String renamedAttribute) { + this.renamedAttribute = renamedAttribute; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((key == null) ? 0 : key.hashCode()); + result = prime * result + ((renamedAttribute == null) ? 0 : renamedAttribute.hashCode()); + result = prime * result + ((stringAttribute == null) ? 0 : stringAttribute.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) + return true; + if ( obj == null ) + return false; + if ( getClass() != obj.getClass() ) + return false; + StringAttributeClass other = (StringAttributeClass) obj; + if ( key == null ) { + if ( other.key != null ) + return false; + } else if ( !key.equals(other.key) ) + return false; + if ( renamedAttribute == null ) { + if ( other.renamedAttribute != null ) + return false; + } else if ( !renamedAttribute.equals(other.renamedAttribute) ) + return false; + if ( stringAttribute == null ) { + if ( other.stringAttribute != null ) + return false; + } else if ( !stringAttribute.equals(other.stringAttribute) ) + return false; + return true; + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/StringSetAttributeClass.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/StringSetAttributeClass.java new file mode 100644 index 000000000000..2717a7aeed81 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/StringSetAttributeClass.java @@ -0,0 +1,100 @@ +/* + * 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.mapper.dynamodb.pojos; + +import java.util.Set; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAttribute; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; + +/** + * Test domain class with a string set attribute and a string key + */ +@DynamoDBTable(tableName = "aws-java-sdk-util") +public class StringSetAttributeClass { + + private String key; + private Set stringSetAttribute; + private Set StringSetAttributeRenamed; + + @DynamoDBHashKey + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + @DynamoDBAttribute + public Set getStringSetAttribute() { + return stringSetAttribute; + } + + public void setStringSetAttribute(Set stringSetAttribute) { + this.stringSetAttribute = stringSetAttribute; + } + + @DynamoDBAttribute(attributeName = "originalName") + public Set getStringSetAttributeRenamed() { + return StringSetAttributeRenamed; + } + + public void setStringSetAttributeRenamed(Set stringSetAttributeRenamed) { + StringSetAttributeRenamed = stringSetAttributeRenamed; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((StringSetAttributeRenamed == null) ? 0 : StringSetAttributeRenamed.hashCode()); + result = prime * result + ((key == null) ? 0 : key.hashCode()); + result = prime * result + ((stringSetAttribute == null) ? 0 : stringSetAttribute.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) + return true; + if ( obj == null ) + return false; + if ( getClass() != obj.getClass() ) + return false; + StringSetAttributeClass other = (StringSetAttributeClass) obj; + if ( StringSetAttributeRenamed == null ) { + if ( other.StringSetAttributeRenamed != null ) + return false; + } else if ( !StringSetAttributeRenamed.equals(other.StringSetAttributeRenamed) ) + return false; + if ( key == null ) { + if ( other.key != null ) + return false; + } else if ( !key.equals(other.key) ) + return false; + if ( stringSetAttribute == null ) { + if ( other.stringSetAttribute != null ) + return false; + } else if ( !stringSetAttribute.equals(other.stringSetAttribute) ) + return false; + return true; + } + + + +} \ No newline at end of file diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/SubClass.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/SubClass.java new file mode 100644 index 000000000000..92398cdffc63 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/SubClass.java @@ -0,0 +1,71 @@ +/* + * 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.mapper.dynamodb.pojos; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBDocument; + +import java.nio.ByteBuffer; + +@DynamoDBDocument +public class SubClass { + + private String name; + private Integer value; + private ByteBuffer bb; + + public SubClass() { + name = "name"; + value = 123; + } + + public String getName() { return name; } + public void setName(String n) { name = n; } + + public Integer getValue() { return value; } + public void setValue(Integer i) { value = i; } + + public ByteBuffer getNull() { return bb; } + public void setNull(ByteBuffer b) { bb = b; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof SubClass)) { + return false; + } + + SubClass that = (SubClass) o; + + return equals(this.name, that.name) + && equals(this.value, that.value) + && equals(this.bb, that.bb); + } + + @Override + public String toString() { + return "{name=" + name + ", value=" + value + ", bb=" + bb + "}"; + } + + public static boolean equals(T one, T two) { + if (one == null) { + return (two == null); + } else { + return one.equals(two); + } + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/TestClass.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/TestClass.java new file mode 100644 index 000000000000..12d88d010d81 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/TestClass.java @@ -0,0 +1,180 @@ +/* + * 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.mapper.dynamodb.pojos; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMarshalling; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBNativeBoolean; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; +import software.amazon.awssdk.mapper.dynamodb.RandomUUIDMarshaller; +import software.amazon.awssdk.mapper.dynamodb.S3Link; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +@DynamoDBTable(tableName="nonexisting-test-tablename") +public class TestClass { + + @DynamoDBHashKey + public String getId() { return null; } + public void setId(String id) { } + + public boolean getBoolean() { return false; } + public void setBoolean(boolean value) { } + + public Boolean getBoxedBoolean() { return false; } + public void setBoxedBoolean(Boolean value) { } + + @DynamoDBNativeBoolean + public boolean getNativeBoolean() { return false; } + public void setNativeBoolean(boolean value) { } + + public String getString() { return null; } + public void setString(String value) { } + + public UUID getUuid() { return null; } + public void setUuid(UUID u) { } + + @DynamoDBMarshalling(marshallerClass=RandomUUIDMarshaller.class) + public String getCustomString() { return null; } + public void setCustomString(String s) { } + + public Date getDate() { return null; } + public void setDate(Date d) { } + + public Calendar getCalendar() { return null; } + public void setCalendar(Calendar c) { } + + public byte getByte() { return 0; } + public Byte getBoxedByte() { return 0; } + public void setByte(byte b) { } + public void setBoxedByte(Byte b) { } + + public short getShort() { return 0; } + public Short getBoxedShort() { return 0; } + public void setShort(short s) { } + public void setBoxedShort(Short s) { } + + public int getInt() { return 0; } + public Integer getBoxedInt() { return 0; } + public void setInt(int i) { } + public void setBoxedInt(Integer i) { } + + public long getLong() { return 0; } + public Long getBoxedLong() { return 0l; } + public void setLong(long l) { } + public void setBoxedLong(Long l) { } + + public BigInteger getBigInt() { return BigInteger.ZERO; } + public void setBigInt(BigInteger i) { } + + public float getFloat() { return 0; } + public Float getBoxedFloat() { return 0f; } + public void setFloat(float f) { } + public void setBoxedFloat(Float f) { } + + public double getDouble() { return 0; } + public Double getBoxedDouble() { return 0d; } + public void setDouble(double d) { } + public void setBoxedDouble(Double d) { } + + public BigDecimal getBigDecimal() { return BigDecimal.ZERO; } + public void setBigDecimal(BigDecimal d) { } + + public byte[] getByteArray() { return null; } + public void setByteArray(byte[] b) { } + public ByteBuffer getByteBuffer() { return null; } + public void setByteBuffer(ByteBuffer b) { } + + public Set getBooleanSet() { return null; } + public void setBooleanSet(Set s) { } + + public Set getUuidSet() { return null; } + public void setUuidSet(Set s) { } + + public Set getStringSet() { return null; } + public void setStringSet(Set s) { } + + public Set getDateSet() { return null; } + public void setDateSet(Set d) { } + + public Set getCalendarSet() { return null; } + public void setCalendarSet(Set c) { } + + public Set getByteSet() { return null; } + public void setByteSet(Set s) { } + + public Set getShortSet() { return null; } + public void setShortSet(Set s) { } + + public Set getIntSet() { return null; } + public void setIntSet(Set s) { } + + public Set getLongSet() { return null; } + public void setLongSet(Set s) { } + + public Set getBigIntegerSet() { return null; } + public void setBigIntegerSet(Set s) { } + + public Set getFloatSet() { return null; } + public void setFloatSet(Set s) { } + + public Set getDoubleSet() { return null; } + public void setDoubleSet(Set s) { } + + public Set getBigDecimalSet() { return null; } + public void setBigDecimalSet(Set s) { } + + public Set getByteArraySet() { return null; } + public void setByteArraySet(Set s) { } + + public Set getByteBufferSet() { return null; } + public void setByteBufferSet(Set s) { } + + public Set getObjectSet() { return null; } + public void setObjectSet(Set s) { } + + public List getList() { return null; } + public void setList(List l) { } + + public List getObjectList() { return null; } + public void setObjectList(List l) { } + + public List> getSetList() { return null; } + public void setSetList(List> l) { } + + public Map getMap() { return null; } + public void setMap(Map m) { } + + public Map> getSetMap() { return null; } + public void setSetMap(Map> m) { } + + public Map getBogusMap() { return null; } + public void setBogusMap(Map m) { } + + public SubClass getObject() { return null; } + public void setObject(SubClass c) { } + + public S3Link getS3Link() { return null; } + public void setS3Link(S3Link link) { } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/TestDocClass.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/TestDocClass.java new file mode 100644 index 000000000000..8e774e6c8bc7 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/TestDocClass.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.mapper.dynamodb.pojos; + +import java.util.List; +import java.util.Map; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBDocument; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; + +@DynamoDBTable(tableName="aws-java-sdk-doc-attribute-table") +public class TestDocClass { + private String id; + private List> listOfMaps; + private Map> mapOfLists; + + @DynamoDBHashKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public List> getListOfMaps() { + return listOfMaps; + } + + public void setListOfMaps(List> listOfMaps) { + this.listOfMaps = listOfMaps; + } + + public Map> getMapOfLists() { + return mapOfLists; + } + + public void setMapOfLists(Map> mapOfLists) { + this.mapOfLists = mapOfLists; + } + + @Override + public boolean equals(Object obj) { + TestDocClass other = (TestDocClass) obj; + + return (eq(id, other.id) + && eq(listOfMaps, other.listOfMaps) + && eq(mapOfLists, other.mapOfLists)); + } + + @Override + public String toString() { + return "{id=" + id + ", listOfMaps=" + listOfMaps + ", mapOfLists=" + + mapOfLists + "}"; + } + + @DynamoDBDocument + public static class ChildClass { + + private boolean bool; + + private ChildClass firstChild; + private List otherChildren; + private Map namedChildren; + + public boolean isBool() { + return bool; + } + + public void setBool(boolean bool) { + this.bool = bool; + } + + public ChildClass getFirstChild() { + return firstChild; + } + + public void setFirstChild(ChildClass firstChild) { + this.firstChild = firstChild; + } + + public List getOtherChildren() { + return otherChildren; + } + + public void setOtherChildren(List otherChildren) { + this.otherChildren = otherChildren; + } + + public Map getNamedChildren() { + return namedChildren; + } + + public void setNamedChildren(Map namedChildren) { + this.namedChildren = namedChildren; + } + + @Override + public boolean equals(Object obj) { + ChildClass other = (ChildClass) obj; + + return (eq(bool, other.bool) + && eq(firstChild, other.firstChild) + && eq(otherChildren, other.otherChildren) + && eq(namedChildren, other.namedChildren)); + } + + @Override + public String toString() { + return "{bool=" + bool + ", firstChild=" + firstChild + + ", otherChildren=" + otherChildren + ", namedChildren=" + + namedChildren + "}"; + } + } + + protected static boolean eq(T one, T two) { + if (one == null) { + return (two == null); + } else { + return one.equals(two); + } + } +} + + diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/TestItem.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/TestItem.java new file mode 100644 index 000000000000..34d0fcb6eab6 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/TestItem.java @@ -0,0 +1,133 @@ +/* + * 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.mapper.dynamodb.pojos; + +import java.util.Set; + +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAttribute; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBRangeKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; + +@DynamoDBTable(tableName = TestItem.TABLE_NAME) +public class TestItem { + + public static final String TABLE_NAME = "aws-java-sdk-dynamodb-mapper-save-config-test"; + + private String hashKey; + private Long rangeKey; + private String stringAttribute; + private String nonKeyAttribute; + private Set stringSetAttribute; + + @DynamoDBHashKey(attributeName = "hashKey") + public String getHashKey() { + return hashKey; + } + + public void setHashKey(String hashKey) { + this.hashKey = hashKey; + } + + @DynamoDBRangeKey(attributeName = "rangeKey") + public Long getRangeKey() { + return rangeKey; + } + + public void setRangeKey(Long rangeKey) { + this.rangeKey = rangeKey; + } + + @DynamoDBAttribute(attributeName = "nonKeyAttribute") + public String getNonKeyAttribute() { + return nonKeyAttribute; + } + + public void setNonKeyAttribute(String nonKeyAttribute) { + this.nonKeyAttribute = nonKeyAttribute; + } + + @DynamoDBAttribute + public String getStringAttribute() { + return stringAttribute; + } + + public void setStringAttribute(String stringAttribute) { + this.stringAttribute = stringAttribute; + } + + + @DynamoDBAttribute(attributeName = "stringSetAttribute") + public Set getStringSetAttribute() { + return stringSetAttribute; + } + + public void setStringSetAttribute(Set stringSetAttribute) { + this.stringSetAttribute = stringSetAttribute; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((hashKey == null) ? 0 : hashKey.hashCode()); + result = prime * result + ((rangeKey == null) ? 0 : rangeKey.hashCode()); + result = prime * result + ((nonKeyAttribute == null) ? 0 : nonKeyAttribute.hashCode()); + result = prime * result + ((stringAttribute == null) ? 0 : stringAttribute.hashCode()); + result = prime * result + ((stringSetAttribute == null) ? 0 : stringSetAttribute.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) + return true; + if ( obj == null ) + return false; + if ( getClass() != obj.getClass() ) + return false; + TestItem other = (TestItem) obj; + if ( hashKey == null ) { + if ( other.hashKey != null ) + return false; + } else if ( !hashKey.equals(other.hashKey) ) + return false; + if ( rangeKey == null ) { + if ( other.rangeKey != null ) + return false; + } else if ( !rangeKey.equals(other.rangeKey) ) + return false; + if ( nonKeyAttribute == null ) { + if ( other.nonKeyAttribute != null ) + return false; + } else if ( !nonKeyAttribute.equals(other.nonKeyAttribute) ) + return false; + if ( stringSetAttribute == null ) { + if ( other.stringSetAttribute != null ) + return false; + } else if ( !stringSetAttribute.equals(other.stringSetAttribute) ) + return false; + return true; + } + + @Override + public String toString() { + return "TestItem: {hashKey=" + hashKey + ", rangeKey=" + rangeKey + + ", stringAttribute=" + stringAttribute + ", stringSetAttribute=" + + stringSetAttribute + ", nonKeyAttribute=" + nonKeyAttribute + "}"; + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/UnannotatedSubClass.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/UnannotatedSubClass.java new file mode 100644 index 000000000000..a816ccfdab4a --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/pojos/UnannotatedSubClass.java @@ -0,0 +1,21 @@ +/* + * 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.mapper.dynamodb.pojos; + +public class UnannotatedSubClass { + public UnannotatedSubClass getChild() { return null; } + public void setChild(UnannotatedSubClass c) { } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/shape/ShapeItems.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/shape/ShapeItems.java new file mode 100644 index 000000000000..245d4863c444 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/shape/ShapeItems.java @@ -0,0 +1,407 @@ +/* + * 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.mapper.dynamodb.shape; + +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Set; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBAttribute; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBDocument; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBFlattened; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBHashKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBNativeBoolean; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBRangeKey; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTable; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTypeConvertedEnum; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBVersionAttribute; + +// Mapper-annotated test POJOs shared by ShapeRequestTest and ShapeResponseTest. +final class ShapeItems { + + private ShapeItems() { + } + + // Hash key plus one string attribute; the grammar workhorse. + @DynamoDBTable(tableName = "M_String") + public static class StringItem { + private String id; + private String value; + + @DynamoDBHashKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @DynamoDBAttribute + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } + + // Hash + range key for composite-key and range-condition cases. + @DynamoDBTable(tableName = "M_Range") + public static class RangeItem { + private String id; + private String range; + + @DynamoDBHashKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @DynamoDBRangeKey + public String getRange() { + return range; + } + + public void setRange(String range) { + this.range = range; + } + } + + // Version-attributed, for the auto-generated version guard. + @DynamoDBTable(tableName = "M_Versioned") + public static class VersionedItem { + private String id; + private Long version; + + @DynamoDBHashKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @DynamoDBVersionAttribute + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + } + + // Nested @DynamoDBDocument rendered as an M. + @DynamoDBDocument + public static class Address { + private String city; + private Long zip; + + @DynamoDBAttribute + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + @DynamoDBAttribute + public Long getZip() { + return zip; + } + + public void setZip(Long zip) { + this.zip = zip; + } + } + + public enum Color { + RED, GREEN, BLUE + } + + // One field per value encoding; a case sets one field and saves with PUT so each fixture isolates a single encoding. + @DynamoDBTable(tableName = "M_AllTypes") + public static class AllTypesItem { + private String id; + private Long number; + private Double doubleValue; + private Boolean numericBool; + private Boolean nativeBool; + private Set stringSet; + private Set numberSet; + private List list; + private Map map; + private Address document; + private Color enumValue; + + @DynamoDBHashKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public AllTypesItem withId(String id) { + this.id = id; + return this; + } + + @DynamoDBAttribute + public Long getNumber() { + return number; + } + + public void setNumber(Long number) { + this.number = number; + } + + public AllTypesItem withNumber(Long number) { + this.number = number; + return this; + } + + @DynamoDBAttribute(attributeName = "double") + public Double getDoubleValue() { + return doubleValue; + } + + public void setDoubleValue(Double doubleValue) { + this.doubleValue = doubleValue; + } + + public AllTypesItem withDouble(Double doubleValue) { + this.doubleValue = doubleValue; + return this; + } + + // Plain Boolean, encoded as N 0/1. + @DynamoDBAttribute + public Boolean getNumericBool() { + return numericBool; + } + + public void setNumericBool(Boolean numericBool) { + this.numericBool = numericBool; + } + + public AllTypesItem withNumericBool(Boolean numericBool) { + this.numericBool = numericBool; + return this; + } + + // Encoded as a native BOOL. + @DynamoDBNativeBoolean + @DynamoDBAttribute + public Boolean getNativeBool() { + return nativeBool; + } + + public void setNativeBool(Boolean nativeBool) { + this.nativeBool = nativeBool; + } + + public AllTypesItem withNativeBool(Boolean nativeBool) { + this.nativeBool = nativeBool; + return this; + } + + @DynamoDBAttribute + public Set getStringSet() { + return stringSet; + } + + public void setStringSet(Set stringSet) { + this.stringSet = stringSet; + } + + public AllTypesItem withStringSet(Set stringSet) { + this.stringSet = stringSet; + return this; + } + + @DynamoDBAttribute + public Set getNumberSet() { + return numberSet; + } + + public void setNumberSet(Set numberSet) { + this.numberSet = numberSet; + } + + public AllTypesItem withNumberSet(Set numberSet) { + this.numberSet = numberSet; + return this; + } + + @DynamoDBAttribute + public List getList() { + return list; + } + + public void setList(List list) { + this.list = list; + } + + public AllTypesItem withList(List list) { + this.list = list; + return this; + } + + @DynamoDBAttribute + public Map getMap() { + return map; + } + + public void setMap(Map map) { + this.map = map; + } + + public AllTypesItem withMap(Map map) { + this.map = map; + return this; + } + + @DynamoDBAttribute + public Address getDocument() { + return document; + } + + public void setDocument(Address document) { + this.document = document; + } + + public AllTypesItem withDocument(Address document) { + this.document = document; + return this; + } + + @DynamoDBTypeConvertedEnum + @DynamoDBAttribute(attributeName = "enum") + public Color getEnumValue() { + return enumValue; + } + + public void setEnumValue(Color enumValue) { + this.enumValue = enumValue; + } + + public AllTypesItem withEnumValue(Color enumValue) { + this.enumValue = enumValue; + return this; + } + } + + // Default Date maps to an ISO-8601 S; the epoch and pattern variants have their own annotations. + @DynamoDBTable(tableName = "M_Dated") + public static class DatedItem { + private String id; + private Date createdAt; + + @DynamoDBHashKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @DynamoDBAttribute + public Date getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Date createdAt) { + this.createdAt = createdAt; + } + } + + // A nested bean whose single field is flattened into the parent item as a sibling attribute. + @DynamoDBDocument + public static class Name { + private String first; + + @DynamoDBAttribute + public String getFirst() { + return first; + } + + public void setFirst(String first) { + this.first = first; + } + } + + @DynamoDBTable(tableName = "M_Flattened") + public static class FlattenedItem { + private String id; + private Name name; + + @DynamoDBHashKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @DynamoDBFlattened(attributes = @DynamoDBAttribute(mappedBy = "first", attributeName = "firstName")) + public Name getName() { + return name; + } + + public void setName(Name name) { + this.name = name; + } + } + + // Hash key and table declared on the base; the subclass inherits both and only adds one attribute. + @DynamoDBTable(tableName = "M_Inherited") + public static class BaseKeyed { + private String id; + + @DynamoDBHashKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + } + + public static class InheritedItem extends BaseKeyed { + private String label; + + @DynamoDBAttribute + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/shape/ShapeRequestTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/shape/ShapeRequestTest.java new file mode 100644 index 000000000000..55db5eecd262 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/shape/ShapeRequestTest.java @@ -0,0 +1,483 @@ +/* + * 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.mapper.dynamodb.shape; + +import static software.amazon.awssdk.mapper.dynamodb.shape.ShapeSupport.HASH_KEY; +import static software.amazon.awssdk.mapper.dynamodb.shape.ShapeSupport.n; +import static software.amazon.awssdk.mapper.dynamodb.shape.ShapeSupport.s; +import static software.amazon.awssdk.mapper.dynamodb.shape.ShapeSupport.verify; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.core.exception.AbortedException; +import software.amazon.awssdk.core.exception.SdkException; +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.sync.RequestBody; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBDeleteExpression; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig.SaveBehavior; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBQueryExpression; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBSaveExpression; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBScanExpression; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBTransactionWriteExpression; +import software.amazon.awssdk.mapper.dynamodb.TransactionLoadRequest; +import software.amazon.awssdk.mapper.dynamodb.TransactionWriteRequest; +import software.amazon.awssdk.mapper.dynamodb.shape.ShapeItems.Address; +import software.amazon.awssdk.mapper.dynamodb.shape.ShapeItems.AllTypesItem; +import software.amazon.awssdk.mapper.dynamodb.shape.ShapeItems.Color; +import software.amazon.awssdk.mapper.dynamodb.shape.ShapeItems.DatedItem; +import software.amazon.awssdk.mapper.dynamodb.shape.ShapeItems.FlattenedItem; +import software.amazon.awssdk.mapper.dynamodb.shape.ShapeItems.InheritedItem; +import software.amazon.awssdk.mapper.dynamodb.shape.ShapeItems.Name; +import software.amazon.awssdk.mapper.dynamodb.shape.ShapeItems.RangeItem; +import software.amazon.awssdk.mapper.dynamodb.shape.ShapeItems.StringItem; +import software.amazon.awssdk.mapper.dynamodb.shape.ShapeItems.VersionedItem; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.ComparisonOperator; +import software.amazon.awssdk.services.dynamodb.model.Condition; +import software.amazon.awssdk.services.dynamodb.model.ExpectedAttributeValue; +import software.amazon.awssdk.services.dynamodb.model.Select; +import software.amazon.awssdk.utils.IoUtils; + +/** Captures each mapper call's marshalled request (X-Amz-Target + JSON body) and asserts it against a committed fixture. */ +@RunWith(Parameterized.class) +public class ShapeRequestTest { + + private static final String TARGET_HEADER = "X-Amz-Target"; + + /** The DynamoDB operation a case targets, and the fixture file it asserts against. */ + enum Operation { + PUT_ITEM("put_item_fixture.json"), + UPDATE_ITEM("update_item_fixture.json"), + DELETE_ITEM("delete_item_fixture.json"), + GET_ITEM("get_item_fixture.json"), + QUERY("query_fixture.json"), + SCAN("scan_fixture.json"), + BATCH_WRITE_ITEM("batch_write_item_fixture.json"), + BATCH_GET_ITEM("batch_get_item_fixture.json"), + TRANSACT_WRITE_ITEMS("transaction_write_fixture.json"), + TRANSACT_GET_ITEMS("transaction_load_fixture.json"); + + final String fixture; + + Operation(String fixture) { + this.fixture = fixture; + } + } + + interface MapperAction { + void run(DynamoDBMapper mapper); + } + + static final class Case { + final Operation operation; + final String name; + final MapperAction action; + + Case(Operation operation, String name, MapperAction action) { + this.operation = operation; + this.name = name; + this.action = action; + } + + @Override + public String toString() { + return operation.fixture.replace("_fixture.json", "") + ":" + name; + } + } + + @Parameters(name = "{0}") + public static List cases() { + List c = new ArrayList<>(); + + // PutItem value matrix: one field per case, saved with PUT so each fixture isolates a single encoding. + add(c, "string/normal", m -> m.save(stringItem("hello"), put())); + add(c, "string/unicode", m -> m.save(stringItem("héllo-世界-😀"), put())); + add(c, "string/whitespace", m -> m.save(stringItem(" pad \t\n"), put())); + add(c, "number/positive", m -> m.save(at().withNumber(42L), put())); + add(c, "number/zero", m -> m.save(at().withNumber(0L), put())); + add(c, "number/negative", m -> m.save(at().withNumber(-7L), put())); + add(c, "number/max-long", m -> m.save(at().withNumber(Long.MAX_VALUE), put())); + add(c, "number/min-long", m -> m.save(at().withNumber(Long.MIN_VALUE), put())); + add(c, "double/fractional", m -> m.save(at().withDouble(1.5d), put())); + add(c, "double/whole", m -> m.save(at().withDouble(1.0d), put())); + add(c, "double/negative", m -> m.save(at().withDouble(-0.25d), put())); + add(c, "bool-numeric/true", m -> m.save(at().withNumericBool(true), put())); + add(c, "bool-numeric/false", m -> m.save(at().withNumericBool(false), put())); + add(c, "bool-native/true", m -> m.save(at().withNativeBool(true), put())); + add(c, "bool-native/false", m -> m.save(at().withNativeBool(false), put())); + add(c, "string-set/single", m -> m.save(at().withStringSet(set("a")), put())); + add(c, "string-set/multi", m -> m.save(at().withStringSet(set("c", "a", "b")), put())); + add(c, "number-set/single", m -> m.save(at().withNumberSet(set(1L)), put())); + add(c, "number-set/multi", m -> m.save(at().withNumberSet(set(3L, 1L, 2L)), put())); + add(c, "list/single", m -> m.save(at().withList(list("only")), put())); + add(c, "list/multi", m -> m.save(at().withList(list("first", "second", "third")), put())); + add(c, "map/single", m -> m.save(at().withMap(map("k", "v")), put())); + add(c, "map/multi", m -> m.save(at().withMap(map("b", "2", "a", "1")), put())); + // Single-attribute document: a multi-field @DynamoDBDocument marshals in reflection order, which varies across + // JVMs and can't be byte-pinned. One field proves the M encoding without ordering ambiguity. + add(c, "document/populated", m -> m.save(at().withDocument(city("Seattle")), put())); + add(c, "enum/value", m -> m.save(at().withEnumValue(Color.GREEN), put())); + + // Single-field flattened doc: the nested Name.first is hoisted to a sibling "firstName" attribute. + c.add(new Case(Operation.PUT_ITEM, "flattened", m -> m.save(flattenedItem("Ada"), put()))); + // Hash key declared on a base class still marshals into the key envelope. + c.add(new Case(Operation.PUT_ITEM, "inherited-key", m -> m.save(inheritedItem("tag"), put()))); + // Default Date with no epoch/pattern annotation marshals as an ISO-8601 S. Fixed instant for determinism. + c.add(new Case(Operation.PUT_ITEM, "date-iso8601", + m -> m.save(datedItem(new Date(1_600_000_000_000L)), put()))); + + c.add(new Case(Operation.UPDATE_ITEM, "save-default-update", m -> m.save(stringItem("hello")))); + c.add(new Case(Operation.UPDATE_ITEM, "versioned-first-save", m -> m.save(versionedItem()))); + + c.add(new Case(Operation.DELETE_ITEM, "simple", m -> m.delete(stringItem("hello")))); + c.add(new Case(Operation.DELETE_ITEM, "versioned", m -> m.delete(versionedItemWithVersion()))); + + c.add(new Case(Operation.GET_ITEM, "by-key", m -> m.load(StringItem.class, HASH_KEY))); + + c.add(new Case(Operation.QUERY, "hash-key-equals", m -> { + StringItem key = new StringItem(); + key.setId(HASH_KEY); + m.query(StringItem.class, new DynamoDBQueryExpression().withHashKeyValues(key)); + })); + + c.add(new Case(Operation.SCAN, "unfiltered", m -> m.scan(StringItem.class, new DynamoDBScanExpression()))); + + c.add(new Case(Operation.BATCH_WRITE_ITEM, "two-puts", + m -> m.batchWrite(Arrays.asList(stringItem("a"), stringItem("b")), Collections.emptyList()))); + + c.add(new Case(Operation.BATCH_GET_ITEM, "two-keys", + m -> m.batchLoad(Arrays.asList(keyOnly("a"), keyOnly("b"))))); + + // CLOBBER vs PUT diverge only on a versioned item: PUT keeps the Expected version guard, CLOBBER suppresses it. + c.add(new Case(Operation.PUT_ITEM, "clobber-suppresses-version-check", + m -> m.save(versionedItem(), new DynamoDBMapperConfig(SaveBehavior.CLOBBER)))); + c.add(new Case(Operation.PUT_ITEM, "put-keeps-version-check", + m -> m.save(versionedItem(), new DynamoDBMapperConfig(SaveBehavior.PUT)))); + c.add(new Case(Operation.UPDATE_ITEM, "append-set-behavior", + m -> m.save(at().withStringSet(set("a", "b")), new DynamoDBMapperConfig(SaveBehavior.APPEND_SET)))); + + // UPDATE emits Action:DELETE for a null attribute; SKIP_NULL omits it. + c.add(new Case(Operation.UPDATE_ITEM, "null-attr-delete", + m -> m.save(stringItem(null), new DynamoDBMapperConfig(SaveBehavior.UPDATE)))); + c.add(new Case(Operation.UPDATE_ITEM, "null-attr-skip", + m -> m.save(stringItem(null), new DynamoDBMapperConfig(SaveBehavior.UPDATE_SKIP_NULL_ATTRIBUTES)))); + + c.add(new Case(Operation.GET_ITEM, "composite-key", m -> m.load(RangeItem.class, HASH_KEY, "r"))); + + c.add(new Case(Operation.UPDATE_ITEM, "save-with-expected-expression", + m -> m.save(stringItem("hello"), new DynamoDBSaveExpression() + .withExpectedEntry("value", ExpectedAttributeValue.builder().exists(false).build())))); + + c.add(new Case(Operation.DELETE_ITEM, "delete-with-expected-expression", + m -> m.delete(stringItem("hello"), new DynamoDBDeleteExpression() + .withExpectedEntry("value", ExpectedAttributeValue.builder().value(s("hello")).build())))); + c.add(new Case(Operation.DELETE_ITEM, "delete-with-condition-expression", + m -> m.delete(stringItem("hello"), new DynamoDBDeleteExpression() + .withConditionExpression("attribute_exists(#v)") + .withExpressionAttributeNames(Collections.singletonMap("#v", "value"))))); + + c.add(new Case(Operation.QUERY, "range-condition-limit-desc", m -> { + RangeItem key = new RangeItem(); + key.setId(HASH_KEY); + Condition rangeCond = Condition.builder() + .comparisonOperator(ComparisonOperator.GT) + .attributeValueList(n("5")) + .build(); + m.query(RangeItem.class, new DynamoDBQueryExpression() + .withHashKeyValues(key) + .withRangeKeyCondition("range", rangeCond) + .withLimit(10) + .withScanIndexForward(false)); + })); + c.add(new Case(Operation.QUERY, "consistent-read-select", m -> { + StringItem key = new StringItem(); + key.setId(HASH_KEY); + m.query(StringItem.class, new DynamoDBQueryExpression() + .withHashKeyValues(key) + .withConsistentRead(true) + .withSelect(Select.ALL_ATTRIBUTES)); + })); + c.add(new Case(Operation.QUERY, "query-filter", m -> { + StringItem key = new StringItem(); + key.setId(HASH_KEY); + m.query(StringItem.class, new DynamoDBQueryExpression() + .withHashKeyValues(key) + .withQueryFilterEntry("value", Condition.builder() + .comparisonOperator(ComparisonOperator.EQ) + .attributeValueList(s("hello")) + .build())); + })); + c.add(new Case(Operation.QUERY, "exclusive-start-key", m -> { + StringItem key = new StringItem(); + key.setId(HASH_KEY); + Map start = new LinkedHashMap<>(); + start.put("id", s(HASH_KEY)); + m.query(StringItem.class, new DynamoDBQueryExpression() + .withHashKeyValues(key) + .withExclusiveStartKey(start)); + })); + c.add(new Case(Operation.QUERY, "projection-expression", m -> { + StringItem key = new StringItem(); + key.setId(HASH_KEY); + m.query(StringItem.class, new DynamoDBQueryExpression() + .withHashKeyValues(key) + .withProjectionExpression("#v") + .withExpressionAttributeNames(Collections.singletonMap("#v", "value"))); + })); + // keyConditionExpression is mutually exclusive with hashKeyValues; the mapper throws if both are set. + c.add(new Case(Operation.QUERY, "key-condition-expression", + m -> m.query(StringItem.class, new DynamoDBQueryExpression() + .withKeyConditionExpression("#i = :v") + .withExpressionAttributeNames(Collections.singletonMap("#i", "id")) + .withExpressionAttributeValues(Collections.singletonMap(":v", s(HASH_KEY)))))); + + c.add(new Case(Operation.SCAN, "filtered-limited", m -> m.scan(StringItem.class, + new DynamoDBScanExpression() + .withFilterConditionEntry("value", Condition.builder() + .comparisonOperator(ComparisonOperator.EQ) + .attributeValueList(s("hello")) + .build()) + .withLimit(25)))); + c.add(new Case(Operation.SCAN, "consistent-read", m -> m.scan(StringItem.class, + new DynamoDBScanExpression().withConsistentRead(true)))); + c.add(new Case(Operation.SCAN, "projection-expression", m -> m.scan(StringItem.class, + new DynamoDBScanExpression() + .withProjectionExpression("#v") + .withExpressionAttributeNames(Collections.singletonMap("#v", "value"))))); + c.add(new Case(Operation.SCAN, "filter-expression", m -> m.scan(StringItem.class, + new DynamoDBScanExpression() + .withFilterExpression("#v = :v") + .withExpressionAttributeNames(Collections.singletonMap("#v", "value")) + .withExpressionAttributeValues(Collections.singletonMap(":v", s("hello")))))); + c.add(new Case(Operation.SCAN, "segment", m -> m.scan(StringItem.class, + new DynamoDBScanExpression().withTotalSegments(4).withSegment(1)))); + + c.add(new Case(Operation.BATCH_WRITE_ITEM, "mixed-put-delete", + m -> m.batchWrite(Arrays.asList(stringItem("p")), Arrays.asList(keyOnly("d"))))); + + // Fixed idempotency token: an unset one makes the core SDK auto-fill ClientRequestToken with a random UUID. + c.add(new Case(Operation.TRANSACT_WRITE_ITEMS, "put-update-delete", m -> { + TransactionWriteRequest req = new TransactionWriteRequest() + .addPut(stringItem("p")) + .addUpdate(stringItem("u")) + .addDelete(keyOnly("d")) + .withIdempotencyToken("fixed-token"); + m.transactionWrite(req); + })); + c.add(new Case(Operation.TRANSACT_WRITE_ITEMS, "condition-check", m -> { + TransactionWriteRequest req = new TransactionWriteRequest() + .addConditionCheck(keyOnly("c"), new DynamoDBTransactionWriteExpression() + .withConditionExpression("attribute_exists(#i)") + .withExpressionAttributeNames(Collections.singletonMap("#i", "id"))) + .withIdempotencyToken("fixed-token"); + m.transactionWrite(req); + })); + // Versioned put with no user expression: the mapper auto-generates the version condition. + c.add(new Case(Operation.TRANSACT_WRITE_ITEMS, "versioned-auto-condition", m -> { + TransactionWriteRequest req = new TransactionWriteRequest() + .addPut(versionedItem()) + .withIdempotencyToken("fixed-token"); + m.transactionWrite(req); + })); + + c.add(new Case(Operation.TRANSACT_GET_ITEMS, "two-loads", m -> { + TransactionLoadRequest req = new TransactionLoadRequest() + .addLoad(keyOnly("a")) + .addLoad(keyOnly("b")); + m.transactionLoad(req); + })); + + return c; + } + + @Parameter + public Case testCase; + + @Test + public void matchesFixture() { + verify(testCase.operation.fixture, testCase.name, captureRequest(testCase.action)); + } + + // Captures the marshalled request as "target\nbody" via an afterMarshalling interceptor that aborts before transmission. + private static String captureRequest(MapperAction action) { + String[] captured = new String[1]; + ExecutionInterceptor handler = new ExecutionInterceptor() { + @Override + public void afterMarshalling(Context.AfterMarshalling context, ExecutionAttributes executionAttributes) { + String target = context.httpRequest().firstMatchingHeader(TARGET_HEADER) + .orElseThrow(() -> new IllegalStateException("Marshalled request had no " + TARGET_HEADER + " header")); + captured[0] = target + "\n" + readContent(context.requestBody().orElse(null)); + throw new StopSignal(); + } + }; + DynamoDbClient client = DynamoDbClient.builder() + .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("akid", "skid"))) + .region(Region.US_EAST_1) + .endpointOverride(URI.create("http://localhost:8000")) + .overrideConfiguration(c -> c.addExecutionInterceptor(handler)) + .build(); + DynamoDBMapper mapper = new DynamoDBMapper(client); + try { + action.run(mapper); + } catch (StopSignal expected) { + // request captured; network intentionally aborted + } catch (AbortedException expected) { + // the core wraps the interceptor throw before transmission; request already captured + } catch (SdkException e) { + if (findStopSignal(e) == null) { + throw new IllegalStateException("Request reached the network instead of being captured", e); + } + } + if (captured[0] == null) { + throw new IllegalStateException("No DynamoDB request was marshalled by the action"); + } + return captured[0]; + } + + // The core may wrap an interceptor-thrown exception; walk the cause chain for our StopSignal. + private static StopSignal findStopSignal(Throwable t) { + for (Throwable c = t; c != null; c = c.getCause()) { + if (c instanceof StopSignal) { + return (StopSignal) c; + } + } + return null; + } + + private static final class StopSignal extends RuntimeException { + StopSignal() { + super(null, null, false, false); + } + } + + private static String readContent(RequestBody body) { + if (body == null) { + throw new IllegalStateException("Marshalled request had no body"); + } + try (InputStream content = body.contentStreamProvider().newStream()) { + return new String(IoUtils.toByteArray(content), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private static void add(List c, String name, MapperAction action) { + c.add(new Case(Operation.PUT_ITEM, name, action)); + } + + private static DynamoDBMapperConfig put() { + return new DynamoDBMapperConfig(SaveBehavior.PUT); + } + + private static AllTypesItem at() { + return new AllTypesItem().withId(HASH_KEY); + } + + private static StringItem stringItem(String v) { + StringItem i = new StringItem(); + i.setId(HASH_KEY); + i.setValue(v); + return i; + } + + private static StringItem keyOnly(String id) { + StringItem i = new StringItem(); + i.setId(id); + return i; + } + + private static VersionedItem versionedItem() { + VersionedItem i = new VersionedItem(); + i.setId(HASH_KEY); + return i; + } + + private static VersionedItem versionedItemWithVersion() { + VersionedItem i = versionedItem(); + i.setVersion(3L); + return i; + } + + private static Address city(String city) { + Address a = new Address(); + a.setCity(city); + return a; + } + + private static FlattenedItem flattenedItem(String first) { + Name name = new Name(); + name.setFirst(first); + FlattenedItem i = new FlattenedItem(); + i.setId(HASH_KEY); + i.setName(name); + return i; + } + + private static InheritedItem inheritedItem(String label) { + InheritedItem i = new InheritedItem(); + i.setId(HASH_KEY); + i.setLabel(label); + return i; + } + + private static DatedItem datedItem(Date createdAt) { + DatedItem i = new DatedItem(); + i.setId(HASH_KEY); + i.setCreatedAt(createdAt); + return i; + } + + @SafeVarargs + private static java.util.Set set(T... v) { + return new LinkedHashSet<>(Arrays.asList(v)); + } + + private static List list(String... v) { + return new ArrayList<>(Arrays.asList(v)); + } + + private static Map map(String... kv) { + Map map = new LinkedHashMap<>(); + for (int j = 0; j < kv.length; j += 2) { + map.put(kv[j], kv[j + 1]); + } + return map; + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/shape/ShapeResponseBehaviorTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/shape/ShapeResponseBehaviorTest.java new file mode 100644 index 000000000000..0e111aea3433 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/shape/ShapeResponseBehaviorTest.java @@ -0,0 +1,157 @@ +/* + * 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.mapper.dynamodb.shape; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBQueryExpression; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBScanExpression; +import software.amazon.awssdk.mapper.dynamodb.QueryResultPage; +import software.amazon.awssdk.mapper.dynamodb.ScanResultPage; +import software.amazon.awssdk.mapper.dynamodb.shape.ShapeItems.StringItem; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; +import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; +import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; +import software.amazon.awssdk.services.dynamodb.model.PutItemResponse; +import software.amazon.awssdk.services.dynamodb.model.QueryRequest; +import software.amazon.awssdk.services.dynamodb.model.QueryResponse; +import software.amazon.awssdk.services.dynamodb.model.ScanRequest; +import software.amazon.awssdk.services.dynamodb.model.ScanResponse; +import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest; +import software.amazon.awssdk.services.dynamodb.model.UpdateItemResponse; + +/** + * Mock behavioral tests for the mapper's response handling control flow. Where ShapeRequestTest pins the marshalled request + * and ShapeResponseTest pins attribute map to POJO unmarshalling + */ +public class ShapeResponseBehaviorTest { + + private static final String HASH_KEY = "1234"; + + private DynamoDbClient ddb; + private DynamoDBMapper mapper; + + @Before + public void setup() { + ddb = mock(DynamoDbClient.class); + mapper = new DynamoDBMapper(ddb); + } + + @Test + public void load_returnsNull_whenItemEmpty() { + when(ddb.getItem(any(GetItemRequest.class))).thenReturn(GetItemResponse.builder().build()); + assertNull(mapper.load(key())); + } + + @Test + public void save_reissuesAsPutItem_whenUpdateReturnsNoAttributes() { + when(ddb.updateItem(any(UpdateItemRequest.class))).thenReturn(UpdateItemResponse.builder().build()); + when(ddb.putItem(any(PutItemRequest.class))).thenReturn(PutItemResponse.builder().build()); + + mapper.save(key()); + + ArgumentCaptor putCaptor = ArgumentCaptor.forClass(PutItemRequest.class); + verify(ddb).putItem(putCaptor.capture()); + assertEquals(HASH_KEY, putCaptor.getValue().item().get("id").s()); + } + + @Test + public void count_sumsAcrossPages_andStopsOnEmptyLastKey() { + Map lastKey = Collections.singletonMap("id", AttributeValue.builder().s(HASH_KEY).build()); + when(ddb.scan(any(ScanRequest.class))) + .thenReturn(ScanResponse.builder().count(3).lastEvaluatedKey(lastKey).build()) + .thenReturn(ScanResponse.builder().count(2).build()); + + assertEquals(5, mapper.count(StringItem.class, new DynamoDBScanExpression())); + verify(ddb, times(2)).scan(any(ScanRequest.class)); + + ArgumentCaptor scanCaptor = ArgumentCaptor.forClass(ScanRequest.class); + verify(ddb, times(2)).scan(scanCaptor.capture()); + assertEquals(lastKey, scanCaptor.getAllValues().get(1).exclusiveStartKey()); + } + + @Test + public void query_lazyPagination_fetchesNextPageUsingLastKey() { + Map lastKey = Collections.singletonMap("id", AttributeValue.builder().s(HASH_KEY).build()); + when(ddb.query(any(QueryRequest.class))) + .thenReturn(QueryResponse.builder().items(item("a")).lastEvaluatedKey(lastKey).build()) + .thenReturn(QueryResponse.builder().items(item("b")).build()); + + Iterator it = mapper.query(StringItem.class, + new DynamoDBQueryExpression().withHashKeyValues(key())).iterator(); + + int seen = 0; + while (it.hasNext()) { + it.next(); + seen++; + } + assertEquals(2, seen); + verify(ddb, times(2)).query(any(QueryRequest.class)); + + ArgumentCaptor queryCaptor = ArgumentCaptor.forClass(QueryRequest.class); + verify(ddb, times(2)).query(queryCaptor.capture()); + assertEquals(lastKey, queryCaptor.getAllValues().get(1).exclusiveStartKey()); + } + + @Test + public void queryPage_lastEvaluatedKeyIsNull_whenServiceReturnsEmptyKey() { + when(ddb.query(any(QueryRequest.class))) + .thenReturn(QueryResponse.builder().items(item("a")).build()); + + QueryResultPage page = mapper.queryPage(StringItem.class, + new DynamoDBQueryExpression().withHashKeyValues(key())); + + assertNull(page.getLastEvaluatedKey()); + } + + @Test + public void scanPage_lastEvaluatedKeyIsNull_whenServiceReturnsEmptyKey() { + when(ddb.scan(any(ScanRequest.class))) + .thenReturn(ScanResponse.builder().items(item("a")).build()); + + ScanResultPage page = mapper.scanPage(StringItem.class, new DynamoDBScanExpression()); + + assertNull(page.getLastEvaluatedKey()); + } + + private static StringItem key() { + StringItem item = new StringItem(); + item.setId(HASH_KEY); + return item; + } + + private static Map item(String id) { + Map m = new HashMap<>(); + m.put("id", AttributeValue.builder().s(id).build()); + return m; + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/shape/ShapeResponseTest.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/shape/ShapeResponseTest.java new file mode 100644 index 000000000000..15cc41bbf2f9 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/shape/ShapeResponseTest.java @@ -0,0 +1,121 @@ +/* + * 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.mapper.dynamodb.shape; + +import static software.amazon.awssdk.mapper.dynamodb.shape.ShapeSupport.HASH_KEY; +import static software.amazon.awssdk.mapper.dynamodb.shape.ShapeSupport.bool; +import static software.amazon.awssdk.mapper.dynamodb.shape.ShapeSupport.entry; +import static software.amazon.awssdk.mapper.dynamodb.shape.ShapeSupport.item; +import static software.amazon.awssdk.mapper.dynamodb.shape.ShapeSupport.l; +import static software.amazon.awssdk.mapper.dynamodb.shape.ShapeSupport.m; +import static software.amazon.awssdk.mapper.dynamodb.shape.ShapeSupport.n; +import static software.amazon.awssdk.mapper.dynamodb.shape.ShapeSupport.ns; +import static software.amazon.awssdk.mapper.dynamodb.shape.ShapeSupport.s; +import static software.amazon.awssdk.mapper.dynamodb.shape.ShapeSupport.ss; +import static software.amazon.awssdk.mapper.dynamodb.shape.ShapeSupport.verify; + +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapper; +import software.amazon.awssdk.mapper.dynamodb.DynamoDBMapperConfig; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.mapper.dynamodb.shape.ShapeItems.AllTypesItem; +import software.amazon.awssdk.mapper.dynamodb.shape.ShapeItems.StringItem; + +// Reconstructs POJOs from canned attribute maps via marshallIntoObject and asserts the result against a fixture. +@RunWith(Parameterized.class) +public class ShapeResponseTest { + + private static final String FIXTURE = "unmarshall_item_fixture.json"; + private static final DynamoDBMapper MAPPER = new DynamoDBMapper((DynamoDbClient) null); + + // NON_NULL keeps the fixture to the reconstructed attributes; alphabetical sort pins the reflection-ordered fields. + private static final ObjectMapper READ_JSON = new ObjectMapper() + .enable(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS) + .enable(SerializationFeature.INDENT_OUTPUT) + .enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY) + .setSerializationInclusion(Include.NON_NULL); + + static final class Case { + final String name; + final Class type; + final Map attributes; + + Case(String name, Class type, Map attributes) { + this.name = name; + this.type = type; + this.attributes = attributes; + } + + @Override + public String toString() { + return "unmarshall:" + name; + } + } + + @Parameters(name = "{0}") + public static List cases() { + List c = new ArrayList<>(); + + c.add(new Case("string", StringItem.class, item("value", s("hello")))); + c.add(new Case("string-unicode", StringItem.class, item("value", s("héllo-世界-😀")))); + c.add(new Case("number", AllTypesItem.class, item("number", n("42")))); + c.add(new Case("number-negative", AllTypesItem.class, item("number", n("-7")))); + c.add(new Case("double", AllTypesItem.class, item("double", n("1.5")))); + c.add(new Case("bool-numeric-true", AllTypesItem.class, item("numericBool", n("1")))); + c.add(new Case("bool-numeric-false", AllTypesItem.class, item("numericBool", n("0")))); + c.add(new Case("bool-native-true", AllTypesItem.class, item("nativeBool", bool(true)))); + c.add(new Case("bool-native-false", AllTypesItem.class, item("nativeBool", bool(false)))); + c.add(new Case("string-set", AllTypesItem.class, item("stringSet", ss("a", "b", "c")))); + c.add(new Case("number-set", AllTypesItem.class, item("numberSet", ns("1", "2", "3")))); + c.add(new Case("list", AllTypesItem.class, item("list", l(s("first"), s("second"))))); + c.add(new Case("map", AllTypesItem.class, item("map", m(entry("a", s("1")), entry("b", s("2")))))); + c.add(new Case("document", AllTypesItem.class, + item("document", m(entry("city", s("Seattle")), entry("zip", n("98101")))))); + c.add(new Case("enum", AllTypesItem.class, item("enum", s("GREEN")))); + + return c; + } + + @Parameter + public Case testCase; + + @Test + public void matchesFixture() { + verify(FIXTURE, testCase.name, reconstruct(testCase.type, testCase.attributes)); + } + + private static String reconstruct(Class type, Map attributes) { + T object = MAPPER.marshallIntoObject(type, attributes, DynamoDBMapperConfig.DEFAULT); + try { + return READ_JSON.writeValueAsString(object); + } catch (JsonProcessingException e) { + throw new IllegalStateException("Failed to serialize reconstructed " + type.getSimpleName(), e); + } + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/shape/ShapeSupport.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/shape/ShapeSupport.java new file mode 100644 index 000000000000..18d779fd422f --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/shape/ShapeSupport.java @@ -0,0 +1,110 @@ +/* + * 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.mapper.dynamodb.shape; + +import static org.junit.Assert.assertEquals; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.utils.IoUtils; + +// Fixture assertion and AttributeValue builders shared by ShapeRequestTest and ShapeResponseTest. +final class ShapeSupport { + + static final String HASH_KEY = "k"; + + private static final ObjectMapper JSON = new ObjectMapper(); + private static final TypeReference> ENTRIES = new TypeReference>() { + }; + private static final Map> CACHE = new ConcurrentHashMap<>(); + + private ShapeSupport() { + } + + // Asserts actual equals the committed fixture entry name in fixtureFile. + static void verify(String fixtureFile, String name, String actual) { + String expected = loadFixture(fixtureFile).get(name); + if (expected == null) { + throw new IllegalStateException("No fixture entry '" + name + "' in " + fixtureFile); + } + assertEquals("Shape drifted for " + fixtureFile + ":" + name, expected, actual); + } + + private static Map loadFixture(String fixtureFile) { + return CACHE.computeIfAbsent(fixtureFile, file -> { + try (InputStream in = ShapeSupport.class.getClassLoader().getResourceAsStream(file)) { + if (in == null) { + throw new IllegalStateException("Missing fixture " + file); + } + return JSON.readValue(IoUtils.toUtf8String(in), ENTRIES); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } + + // A key/response map of hash key id=k plus one named attribute. + static Map item(String attr, AttributeValue value) { + Map map = new LinkedHashMap<>(); + map.put("id", s(HASH_KEY)); + map.put(attr, value); + return map; + } + + static AttributeValue s(String v) { + return AttributeValue.builder().s(v).build(); + } + + static AttributeValue n(String v) { + return AttributeValue.builder().n(v).build(); + } + + static AttributeValue bool(boolean v) { + return AttributeValue.builder().bool(v).build(); + } + + static AttributeValue ss(String... v) { + return AttributeValue.builder().ss(v).build(); + } + + static AttributeValue ns(String... v) { + return AttributeValue.builder().ns(v).build(); + } + + static AttributeValue l(AttributeValue... v) { + return AttributeValue.builder().l(v).build(); + } + + @SafeVarargs + static AttributeValue m(Map.Entry... entries) { + Map map = new LinkedHashMap<>(); + for (Map.Entry e : entries) { + map.put(e.getKey(), e.getValue()); + } + return AttributeValue.builder().m(map).build(); + } + + static Map.Entry entry(String k, AttributeValue v) { + return new java.util.AbstractMap.SimpleEntry<>(k, v); + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/test/AWSTestBase.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/test/AWSTestBase.java new file mode 100644 index 000000000000..b3a498e633f5 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/test/AWSTestBase.java @@ -0,0 +1,206 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.mapper.dynamodb.test; + +import software.amazon.awssdk.mapper.dynamodb.test.retry.RetryRule; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +import software.amazon.awssdk.auth.credentials.AwsCredentials; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProviderChain; +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; +import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider; +import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider; +import software.amazon.awssdk.auth.credentials.SystemPropertyCredentialsProvider; +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.mapper.dynamodb.test.util.InputStreamUtils; +import software.amazon.awssdk.mapper.dynamodb.test.util.SdkAsserts; +import software.amazon.awssdk.utils.IoUtils; +import java.util.concurrent.TimeUnit; + +import software.amazon.awssdk.utils.StringUtils; +import org.junit.Rule; + +public abstract class AWSTestBase { + + /** + * Shared AWS credentials, loaded from a properties file. Direct access to this field is + * deprecated + * + * @deprecated Use the credentials resolved by this base class instead of accessing this field directly + */ + @Deprecated + public static AwsCredentials credentials; + + // Matches the profile that v2's shared AwsIntegrationTestBase/AwsTestBase resolve, and the profile the + // catapult release/PR buildspec writes credentials under (alongside [default]). See + // test/service-test-utils AwsIntegrationTestBase and AwsSdkJava2CatapultCDK test-specs. + private static final String TEST_CREDENTIALS_PROFILE_NAME = "aws-test-account"; + + /** + * ToD test can be configured to use Role ARN and will pull them from STS. These credentials are then available + * for use during the test run. The location of the credentials file is passed to the test run in the form of + * the environment variable TOD_CUSTOMER_CREDENTIAL_PATH. + */ + + private static final String TOD_CREDENTIAL_PATH = System.getenv("TOD_CUSTOMER_CREDENTIAL_PATH"); + + private static final AwsCredentialsProviderChain chain = createChain(); + + @Rule + public RetryRule retry = new RetryRule(3, 2, TimeUnit.SECONDS); + + /** + * @deprecated Use the credentials resolved by this base class instead of calling this directly + */ + @Deprecated + public static void setUpCredentials() { + if (credentials == null) { + try { + credentials = chain.resolveCredentials(); + } catch (Exception ignored) { + } + } + } + + protected void setRetryRule(RetryRule rule) { + this.retry = rule; + } + + /** + * Reads a system resource fully into a String + * + * @param location + * Relative or absolute location of system resource. + * @return String contents of resource file + * @throws RuntimeException + * if any error occurs + */ + protected String getResourceAsString(String location) { + try { + InputStream resourceStream = getClass().getResourceAsStream(location); + String resourceAsString = IoUtils.toUtf8String(resourceStream); + resourceStream.close(); + return resourceAsString; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * @deprecated Use static imports for custom asserts in {@link SdkAsserts} instead + */ + @Deprecated + protected void assertNotEmpty(String str) { + SdkAsserts.assertNotEmpty(str); + } + + /** + * @deprecated Use static imports for custom asserts in {@link SdkAsserts} instead + */ + @Deprecated + protected void assertFileEqualsStream(File expected, InputStream actual) { + SdkAsserts.assertFileEqualsStream(expected, actual); + } + + /** + * @deprecated Use static imports for custom asserts in {@link SdkAsserts} instead + */ + @Deprecated + protected void assertFileEqualsStream(String errmsg, File expected, InputStream actual) { + SdkAsserts.assertFileEqualsStream(errmsg, expected, actual); + } + + /** + * @deprecated Use static imports for custom asserts in {@link SdkAsserts} instead + */ + @Deprecated + protected void assertStreamEqualsStream(InputStream expected, InputStream actual) { + SdkAsserts.assertStreamEqualsStream(expected, actual); + } + + /** + * @deprecated Use static imports for custom asserts in {@link SdkAsserts} instead + */ + @Deprecated + protected void assertStreamEqualsStream(String errmsg, InputStream expectedInputStream, InputStream inputStream) { + assertStreamEqualsStream(errmsg, expectedInputStream, inputStream); + } + + /** + * @deprecated Use static imports for custom asserts in {@link SdkAsserts} instead + */ + @Deprecated + protected void assertFileEqualsFile(File expected, File actual) { + SdkAsserts.assertFileEqualsFile(expected, actual); + } + + /** + * @deprecated Use static imports for custom asserts in {@link SdkAsserts} instead + */ + @Deprecated + protected void assertStringEqualsStream(String expected, InputStream actual) { + SdkAsserts.assertStringEqualsStream(expected, actual); + } + + /** + * @deprecated Use static imports for custom asserts in {@link SdkAsserts} instead + */ + @Deprecated + protected boolean doesStreamEqualStream(InputStream expected, InputStream actual) throws IOException { + return SdkAsserts.doesStreamEqualStream(expected, actual); + } + + /** + * @deprecated Use {@link InputStreamUtils#drainInputStream(InputStream)} + */ + @Deprecated + protected byte[] drainInputStream(InputStream inputStream) { + return InputStreamUtils.drainInputStream(inputStream); + } + + /** + * @deprecated Use static imports for custom asserts in {@link SdkAsserts} instead + */ + @Deprecated + protected boolean doesFileEqualStream(File expectedFile, InputStream inputStream) throws IOException { + return SdkAsserts.doesFileEqualStream(expectedFile, inputStream); + } + + /** + * @deprecated Use static imports for custom asserts in {@link SdkAsserts} instead + */ + @Deprecated + protected void assertValidException(AwsServiceException e) { + SdkAsserts.assertValidException(e); + } + + private static AwsCredentialsProviderChain createChain() { + if (StringUtils.isBlank(TOD_CREDENTIAL_PATH)) { + // Mirror v2's shared AwsIntegrationTestBase: the aws-test-account profile first, then the default + // provider chain (env vars, system properties, the [default] profile, and container/instance creds). + return AwsCredentialsProviderChain.of( + ProfileCredentialsProvider.create(TEST_CREDENTIALS_PROFILE_NAME), + DefaultCredentialsProvider.create()); + } + return AwsCredentialsProviderChain.of( + ProfileCredentialsProvider.create("default"), + ProfileCredentialsProvider.create(TEST_CREDENTIALS_PROFILE_NAME), + EnvironmentVariableCredentialsProvider.create(), + SystemPropertyCredentialsProvider.create()); + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/test/retry/RetryRule.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/test/retry/RetryRule.java new file mode 100644 index 000000000000..92b533e3d634 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/test/retry/RetryRule.java @@ -0,0 +1,63 @@ +/* + * 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.mapper.dynamodb.test.retry; + +import software.amazon.awssdk.utils.Validate; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import java.util.concurrent.TimeUnit; + +public class RetryRule implements TestRule { + + private final int maxRetryAttempts; + private final long delay; + private final TimeUnit timeUnit; + + public RetryRule(int maxRetryAttempts) { + this(maxRetryAttempts, 0, TimeUnit.SECONDS); + } + + public RetryRule(int maxRetryAttempts, long delay, TimeUnit timeUnit) { + this.maxRetryAttempts = maxRetryAttempts; + this.delay = delay; + this.timeUnit = Validate.paramNotNull(timeUnit, "timeUnit"); + } + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + retry(base, 1); + } + + public void retry(final Statement base, int attempts) throws Throwable { + try { + base.evaluate(); + } catch (Exception e) { + if(attempts > maxRetryAttempts) { + throw e; + } + System.out.println("Test failed. Retrying with delay of: " + delay + " " + timeUnit); + timeUnit.sleep(delay); + retry(base, ++attempts); + } + } + }; + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/test/util/DynamoDBIntegrationTestBase.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/test/util/DynamoDBIntegrationTestBase.java new file mode 100644 index 000000000000..bee09913a603 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/test/util/DynamoDBIntegrationTestBase.java @@ -0,0 +1,175 @@ +/* + * 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.mapper.dynamodb.test.util; + +import org.junit.BeforeClass; + +import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition; +import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest; +import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; +import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement; +import software.amazon.awssdk.services.dynamodb.model.KeyType; +import software.amazon.awssdk.services.dynamodb.model.LocalSecondaryIndex; +import software.amazon.awssdk.services.dynamodb.model.Projection; +import software.amazon.awssdk.services.dynamodb.model.ProjectionType; +import software.amazon.awssdk.services.dynamodb.model.ProvisionedThroughput; +import software.amazon.awssdk.services.dynamodb.model.ResourceInUseException; +import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType; + +public class DynamoDBIntegrationTestBase extends DynamoDBTestBase { + + protected static final String KEY_NAME = "key"; + protected static final String TABLE_NAME = "aws-java-sdk-util"; + + protected static long startKey = System.currentTimeMillis(); + + protected static final String TABLE_WITH_RANGE_ATTRIBUTE = "aws-java-sdk-range-test"; + protected static final String TABLE_WITH_INDEX_RANGE_ATTRIBUTE = "aws-java-sdk-index-range-test"; + + @BeforeClass + public static void setUp() throws Exception { + setUpTestBase(); + + // Create a table + String keyName = KEY_NAME; + CreateTableRequest createTableRequest = CreateTableRequest.builder() + .tableName(TABLE_NAME) + .keySchema(KeySchemaElement.builder().attributeName(keyName).keyType(KeyType.HASH).build()) + .attributeDefinitions( + AttributeDefinition.builder().attributeName(keyName) + .attributeType(ScalarAttributeType.S).build()) + .provisionedThroughput(ProvisionedThroughput.builder().readCapacityUnits(10L) + .writeCapacityUnits(5L).build()) + .build(); + + if (createTableIfNotExists(createTableRequest)) { + dynamo.waiter().waitUntilTableExists(b -> b.tableName(TABLE_NAME)); + } + } + + /** + * Creates the table if it does not already exist. Returns true if a create was issued, + * false if the table already existed. + */ + private static boolean createTableIfNotExists(CreateTableRequest createTableRequest) { + try { + dynamo.createTable(createTableRequest); + return true; + } catch (ResourceInUseException e) { + // Table already exists. + return false; + } + } + + protected static void setUpTableWithRangeAttribute() throws Exception { + setUp(); + + String keyName = DynamoDBIntegrationTestBase.KEY_NAME; + String rangeKeyAttributeName = "rangeKey"; + CreateTableRequest createTableRequest = CreateTableRequest.builder() + .tableName(TABLE_WITH_RANGE_ATTRIBUTE) + .keySchema(KeySchemaElement.builder().attributeName(keyName).keyType(KeyType.HASH).build(), + KeySchemaElement.builder().attributeName(rangeKeyAttributeName).keyType(KeyType.RANGE).build()) + .attributeDefinitions( + AttributeDefinition.builder().attributeName(keyName) + .attributeType(ScalarAttributeType.N).build(), + AttributeDefinition.builder().attributeName(rangeKeyAttributeName) + .attributeType(ScalarAttributeType.N).build()) + .provisionedThroughput(ProvisionedThroughput.builder().readCapacityUnits(10L) + .writeCapacityUnits(5L).build()) + .build(); + + if (createTableIfNotExists(createTableRequest)) { + dynamo.waiter().waitUntilTableExists(b -> b.tableName(TABLE_WITH_RANGE_ATTRIBUTE)); + } + } + + protected static void setUpTableWithIndexRangeAttribute(boolean recreateTable) throws Exception { + setUp(); + if (recreateTable) { + dynamo.deleteTable(DeleteTableRequest.builder().tableName(TABLE_WITH_INDEX_RANGE_ATTRIBUTE).build()); + waitForTableToBecomeDeleted(TABLE_WITH_INDEX_RANGE_ATTRIBUTE); + } + + String keyName = DynamoDBIntegrationTestBase.KEY_NAME; + String rangeKeyAttributeName = "rangeKey"; + String indexFooRangeKeyAttributeName = "indexFooRangeKey"; + String indexBarRangeKeyAttributeName = "indexBarRangeKey"; + String multipleIndexRangeKeyAttributeName = "multipleIndexRangeKey"; + String fooAttributeName = "fooAttribute"; + String barAttributeName = "barAttribute"; + String indexFooName = "index_foo"; + String indexBarName = "index_bar"; + String indexFooCopyName = "index_foo_copy"; + String indexBarCopyName = "index_bar_copy"; + + CreateTableRequest createTableRequest = CreateTableRequest.builder() + .tableName(TABLE_WITH_INDEX_RANGE_ATTRIBUTE) + .keySchema( + KeySchemaElement.builder().attributeName(keyName).keyType(KeyType.HASH).build(), + KeySchemaElement.builder().attributeName(rangeKeyAttributeName).keyType(KeyType.RANGE).build()) + .localSecondaryIndexes( + LocalSecondaryIndex.builder() + .indexName(indexFooName) + .keySchema( + KeySchemaElement.builder().attributeName(keyName).keyType(KeyType.HASH).build(), + KeySchemaElement.builder().attributeName(indexFooRangeKeyAttributeName).keyType(KeyType.RANGE).build()) + .projection(Projection.builder() + .projectionType(ProjectionType.INCLUDE) + .nonKeyAttributes(fooAttributeName).build()) + .build(), + LocalSecondaryIndex.builder() + .indexName(indexBarName) + .keySchema( + KeySchemaElement.builder().attributeName(keyName).keyType(KeyType.HASH).build(), + KeySchemaElement.builder().attributeName(indexBarRangeKeyAttributeName).keyType(KeyType.RANGE).build()) + .projection(Projection.builder() + .projectionType(ProjectionType.INCLUDE) + .nonKeyAttributes(barAttributeName).build()) + .build(), + LocalSecondaryIndex.builder() + .indexName(indexFooCopyName) + .keySchema( + KeySchemaElement.builder().attributeName(keyName).keyType(KeyType.HASH).build(), + KeySchemaElement.builder().attributeName(multipleIndexRangeKeyAttributeName).keyType(KeyType.RANGE).build()) + .projection(Projection.builder() + .projectionType(ProjectionType.INCLUDE) + .nonKeyAttributes(fooAttributeName).build()) + .build(), + LocalSecondaryIndex.builder() + .indexName(indexBarCopyName) + .keySchema( + KeySchemaElement.builder().attributeName(keyName).keyType(KeyType.HASH).build(), + KeySchemaElement.builder().attributeName(multipleIndexRangeKeyAttributeName).keyType(KeyType.RANGE).build()) + .projection(Projection.builder() + .projectionType(ProjectionType.INCLUDE) + .nonKeyAttributes(barAttributeName).build()) + .build()) + .attributeDefinitions( + AttributeDefinition.builder().attributeName(keyName).attributeType(ScalarAttributeType.N).build(), + AttributeDefinition.builder().attributeName(rangeKeyAttributeName).attributeType(ScalarAttributeType.N).build(), + AttributeDefinition.builder().attributeName(indexFooRangeKeyAttributeName).attributeType(ScalarAttributeType.N).build(), + AttributeDefinition.builder().attributeName(indexBarRangeKeyAttributeName).attributeType(ScalarAttributeType.N).build(), + AttributeDefinition.builder().attributeName(multipleIndexRangeKeyAttributeName).attributeType(ScalarAttributeType.N).build()) + .provisionedThroughput(ProvisionedThroughput.builder().readCapacityUnits(10L) + .writeCapacityUnits(5L).build()) + .build(); + + if (createTableIfNotExists(createTableRequest)) { + dynamo.waiter().waitUntilTableExists(b -> b.tableName(TABLE_WITH_INDEX_RANGE_ATTRIBUTE)); + } + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/test/util/DynamoDBTestBase.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/test/util/DynamoDBTestBase.java new file mode 100644 index 000000000000..015c659e1254 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/test/util/DynamoDBTestBase.java @@ -0,0 +1,163 @@ +/* + * 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.mapper.dynamodb.test.util; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.math.BigDecimal; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import java.net.URI; + +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.TableDescription; +import software.amazon.awssdk.services.dynamodb.model.TableStatus; +import software.amazon.awssdk.mapper.dynamodb.test.AWSTestBase; + +public class DynamoDBTestBase extends AWSTestBase { + + protected static final String ENDPOINT = "http://dynamodb.us-east-1.amazonaws.com/"; + + protected static DynamoDbClient dynamo; + + /** + * Gets a map of key values for the single hash key attribute value given. + */ + protected Map getMapKey(String attributeName, AttributeValue value) { + HashMap map = new HashMap(); + map.put(attributeName, value); + return map; + } + + public static void setUpTestBase() { + try { + setUpCredentials(); + } catch (Exception e) { + throw SdkClientException.create("Unable to load credential property file.", e); + } + + dynamo = DynamoDbClient.builder() + .credentialsProvider(StaticCredentialsProvider.create(credentials)) + .region(Region.US_EAST_1) + .endpointOverride(URI.create(ENDPOINT)) + .build(); + } + + public static DynamoDbClient getClient() { + if (dynamo == null) { + setUpTestBase(); + } + return dynamo; + } + + protected static void waitForTableToBecomeDeleted(String tableName) { + waitForTableToBecomeDeleted(dynamo, tableName); + } + + public static void waitForTableToBecomeDeleted(DynamoDbClient dynamo, String tableName) { + System.out.println("Waiting for " + tableName + " to become Deleted..."); + + long startTime = System.currentTimeMillis(); + long endTime = startTime + (10 * 60 * 1000); + while ( System.currentTimeMillis() < endTime ) { + try { + Thread.sleep(1000 * 20); + } catch ( Exception e ) { + } + try { + TableDescription table = dynamo.describeTable(b -> b.tableName(tableName)).table(); + + TableStatus tableStatus = table.tableStatus(); + System.out.println(" - current state: " + tableStatus); + if ( tableStatus == TableStatus.DELETING ) + continue; + } catch ( AwsServiceException ase ) { + if ( "ResourceNotFoundException".equalsIgnoreCase(ase.awsErrorDetails().errorCode()) ){ + System.out.println("successfully deleted"); + return; + } + } + } + + throw new RuntimeException("Table " + tableName + " never went deleted"); + } + + protected static void assertSetsEqual(Collection expected, Collection given) { + Set givenCopy = new HashSet(); + givenCopy.addAll(given); + for ( T e : expected ) { + if ( !givenCopy.remove(e) ) { + fail("Expected element not found: " + e); + } + } + + assertTrue("Unexpected elements found: " + givenCopy, givenCopy.isEmpty()); + } + + /** + * Only valid for whole numbers + */ + protected static void assertNumericSetsEquals(Set expected, Collection given) { + Set givenCopy = new HashSet(); + for (String s : given) { + BigDecimal bd = new BigDecimal(s); + givenCopy.add(bd.setScale(0)); + } + + Set expectedCopy = new HashSet(); + for (Number n : expected) { + BigDecimal bd = new BigDecimal(n.toString()); + expectedCopy.add(bd.setScale(0)); + } + + assertSetsEqual(expectedCopy, givenCopy); + } + + protected static Set toSet(T... array) { + Set set = new HashSet(); + for ( T t : array ) { + set.add(t); + } + return set; + } + + protected static Set toSet(Collection collection) { + Set set = new HashSet(); + for ( T t : collection ) { + set.add(t); + } + return set; + } + + protected static byte[] generateByteArray(int length) { + byte [] bytes = new byte[length]; + for (int i = 0; i < length; i++) { + bytes[i] = (byte)(i % Byte.MAX_VALUE); + } + return bytes; + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/test/util/DynamoDBUnitTestBase.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/test/util/DynamoDBUnitTestBase.java new file mode 100644 index 000000000000..2d523e82e69b --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/test/util/DynamoDBUnitTestBase.java @@ -0,0 +1,38 @@ +/* + * 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.mapper.dynamodb.test.util; + +import com.amazonaws.services.dynamodbv2.local.embedded.DynamoDBEmbedded; +import com.amazonaws.services.dynamodbv2.local.shared.access.AmazonDynamoDBLocal; +import org.junit.AfterClass; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; + +public class DynamoDBUnitTestBase extends DynamoDBTestBase { + + protected static AmazonDynamoDBLocal embedded; + protected static DynamoDbClient dynamoDB; + + public static void setUp() { + embedded = DynamoDBEmbedded.create(); + dynamoDB = embedded.dynamoDbClient(); + } + + @AfterClass + public static void shutDown() { + embedded.shutdown(); + } +} + diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/test/util/InputStreamUtils.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/test/util/InputStreamUtils.java new file mode 100644 index 000000000000..b75b9e782345 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/test/util/InputStreamUtils.java @@ -0,0 +1,66 @@ +/* + * 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.mapper.dynamodb.test.util; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import software.amazon.awssdk.utils.IoUtils; + +public class InputStreamUtils { + + /** + * Calculates the MD5 digest for the given input stream and returns it. + */ + public static byte[] calculateMD5Digest(InputStream is) throws NoSuchAlgorithmException, IOException { + int bytesRead = 0; + byte[] buffer = new byte[2048]; + MessageDigest md5 = MessageDigest.getInstance("MD5"); + + while ((bytesRead = is.read(buffer)) != -1) { + md5.update(buffer, 0, bytesRead); + } + return md5.digest(); + } + + /** + * Reads to the end of the inputStream returning a byte array of the contents + * + * @param inputStream + * InputStream to drain + * @return Remaining data in stream as a byte array + */ + public static byte[] drainInputStream(InputStream inputStream) { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + + try { + byte[] buffer = new byte[1024]; + long bytesRead = 0; + while ((bytesRead = inputStream.read(buffer)) > -1) { + byteArrayOutputStream.write(buffer, 0, (int) bytesRead); + } + return byteArrayOutputStream.toByteArray(); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + IoUtils.closeQuietly(byteArrayOutputStream, null); + } + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/test/util/RandomInputStream.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/test/util/RandomInputStream.java new file mode 100644 index 000000000000..1dff8f184dc0 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/test/util/RandomInputStream.java @@ -0,0 +1,116 @@ +/* + * 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.mapper.dynamodb.test.util; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Random; + +/** + * Test utility InputStream implementation that generates random ASCII data when + * read, up to the size specified when constructed. + * + */ +public class RandomInputStream extends InputStream { + + /** The requested amount of data contained in this random stream */ + protected final long lengthInBytes; + + /** The number of bytes of data remaining in this random stream */ + protected long remainingBytes; + + /** Shared Random number generator to generate data */ + private static final Random RANDOM = new Random(); + + /** The minimum ASCII code contained in the data in this stream */ + private static final int MIN_CHAR_CODE = 32; + + /** The maximum ASCII code contained in the data in this stream */ + private static final int MAX_CHAR_CODE = 125; + + /** Flag controlling whether binary or character data is used */ + private final boolean binaryData; + + + /** + * Constructs a new InputStream, which will return the specified amount + * of bytes of random ASCII characters. + * + * @param lengthInBytes + * The size in bytes of the total data returned by this + * stream. + */ + public RandomInputStream(long lengthInBytes) { + this(lengthInBytes, false); + } + + /** + * Creates a new random input stream of specified length, and specifies + * whether the stream should be full on binary or character data. + * + * @param lengthInBytes + * The number of bytes in the stream. + * @param binaryData + * Whether binary or character data should be generated. + */ + public RandomInputStream(long lengthInBytes, boolean binaryData) { + this.lengthInBytes = lengthInBytes; + this.remainingBytes = lengthInBytes; + this.binaryData = binaryData; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + // Signal that we're out of data if we've hit our limit + if (remainingBytes <= 0) return -1; + int bytesToRead = len; + if (bytesToRead > remainingBytes) + bytesToRead = (int) remainingBytes; + + remainingBytes -= bytesToRead; + + if (binaryData) { + byte[] bytes = new byte[bytesToRead]; + RANDOM.nextBytes(bytes); + System.arraycopy(bytes, 0, b, off, bytesToRead); + } else { + for (int i = 0; i < bytesToRead; i++) { + b[off+i] = (byte) (RANDOM.nextInt(MAX_CHAR_CODE - MIN_CHAR_CODE) + MIN_CHAR_CODE); + } + } + + return bytesToRead; + } + + @Override + public int read() throws IOException { + // Signal that we're out of data if we've hit our limit + if (remainingBytes <= 0) return -1; + + remainingBytes--; + if (binaryData) { + byte[] bytes = new byte[1]; + RANDOM.nextBytes(bytes); + return (int)bytes[0]; + } else { + return RANDOM.nextInt(MAX_CHAR_CODE - MIN_CHAR_CODE) + MIN_CHAR_CODE; + } + } + + public long getBytesRead() { + return lengthInBytes - remainingBytes; + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/test/util/RandomTempFile.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/test/util/RandomTempFile.java new file mode 100644 index 000000000000..ee7669658cbe --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/test/util/RandomTempFile.java @@ -0,0 +1,111 @@ +/* + * 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.mapper.dynamodb.test.util; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Extension of File that creates a temporary file with a specified name in + * Java's temporary directory, as declared in the JRE's system properties. The + * file is immediately filled with a specified amount of random ASCII data. + * + * @see RandomInputStream + */ +public class RandomTempFile extends File { + + private static final long serialVersionUID = -8232143353692832238L; + + /** Java temp dir where all temp files will be created */ + private static final String TEMP_DIR = System.getProperty("java.io.tmpdir"); + + /** Flag controlling whether binary or character data is used */ + private final boolean binaryData; + + + /** + * Creates, and fills, a temp file with the specified name and specified + * size of random ASCII data. + * + * @param filename + * The name for the new temporary file, within the Java temp + * directory as declared in the JRE's system properties. + * @param sizeInBytes + * The amount of random ASCII data, in bytes, for the new temp + * file. + * + * @throws IOException + * If any problems were encountered creating the new temp file. + */ + public RandomTempFile(String filename, long sizeInBytes) throws IOException { + this(filename, sizeInBytes, false); + } + + /** + * Creates, and fills, a temp file with the specified name and specified + * size of random binary or character data. + * + * @param filename + * The name for the new temporary file, within the Java temp + * directory as declared in the JRE's system properties. + * @param sizeInBytes + * The amount of random ASCII data, in bytes, for the new temp + * file. + * @param binaryData Whether to fill the file with binary or character data. + * + * @throws IOException + * If any problems were encountered creating the new temp file. + */ + public RandomTempFile(String filename, long sizeInBytes, boolean binaryData) + throws IOException { + super(TEMP_DIR + File.separator + System.currentTimeMillis() + "-" + + filename); + this.binaryData = binaryData; + createFile(sizeInBytes); + System.out.println("RandomTempFile " + this + " created."); + } + + public RandomTempFile(File root, String filename, long sizeInBytes) throws IOException { + super(root, filename); + this.binaryData = false; + createFile( sizeInBytes ); + } + + + public void createFile(long sizeInBytes) throws IOException { + deleteOnExit(); + + FileOutputStream outputStream = new FileOutputStream(this); + BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream); + InputStream inputStream = new RandomInputStream(sizeInBytes, binaryData); + + try { + byte[] buffer = new byte[1024]; + int bytesRead = -1; + while ((bytesRead = inputStream.read(buffer)) > -1) { + bufferedOutputStream.write(buffer, 0, bytesRead); + } + } finally { + bufferedOutputStream.close(); + outputStream.close(); + inputStream.close(); + } + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/test/util/SdkAsserts.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/test/util/SdkAsserts.java new file mode 100644 index 000000000000..433a0b1f5da1 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/test/util/SdkAsserts.java @@ -0,0 +1,245 @@ +/* + * 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.mapper.dynamodb.test.util; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + +import org.junit.Assert; + +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.utils.IoUtils; + +public class SdkAsserts { + + /** + * Asserts that the specified String is not null and not empty. + * + * @param str + * The String to test. + * @deprecated Use Hamcrest Matchers instead + */ + @Deprecated + public static void assertNotEmpty(String str) { + assertNotNull(str); + assertTrue(str.length() > 0); + } + + /** + * Asserts that the contents in the specified file are exactly equal to the contents read from + * the specified input stream. The input stream will be closed at the end of this method. If any + * problems are encountered, or the stream's contents don't match up exactly with the file's + * contents, then this method will fail the current test. + * + * @param expected + * The file containing the expected contents. + * @param actual + * The stream that will be read, compared to the expected file contents, and finally + * closed. + */ + public static void assertFileEqualsStream(File expected, InputStream actual) { + assertFileEqualsStream(null, expected, actual); + } + + /** + * Asserts that the contents in the specified file are exactly equal to the contents read from + * the specified input stream. The input stream will be closed at the end of this method. If any + * problems are encountered, or the stream's contents don't match up exactly with the file's + * contents, then this method will fail the current test. + * + * @param errmsg + * error message to be thrown when the assertion fails. + * @param expected + * The file containing the expected contents. + * @param actual + * The stream that will be read, compared to the expected file contents, and finally + * closed. + */ + public static void assertFileEqualsStream(String errmsg, File expected, InputStream actual) { + try { + InputStream expectedInputStream = new FileInputStream(expected); + assertStreamEqualsStream(errmsg, expectedInputStream, actual); + } catch (FileNotFoundException e) { + fail("Expected file " + expected.getAbsolutePath() + " doesn't exist: " + e.getMessage()); + } + } + + /** + * Asserts that the contents in the specified input streams are same. The input streams will be + * closed at the end of this method. If any problems are encountered, or the stream's contents + * don't match up exactly with the file's contents, then this method will fail the current test. + * + * @param expected + * expected input stream. The stream will be closed at the end. + * @param actual + * The stream that will be read, compared to the expected file contents, and finally + * closed. + */ + public static void assertStreamEqualsStream(InputStream expected, InputStream actual) { + assertStreamEqualsStream(null, expected, actual); + } + + /** + * Asserts that the contents in the specified input streams are same. The input streams will be + * closed at the end of this method. If any problems are encountered, or the stream's contents + * don't match up exactly with the file's contents, then this method will fail the current test. + * + * @param errmsg + * error message to be thrown when the assertion fails. + * @param expected + * expected input stream. The stream will be closed at the end. + * @param actual + * The stream that will be read, compared to the expected file contents, and finally + * closed. + */ + public static void assertStreamEqualsStream(String errmsg, + InputStream expectedInputStream, + InputStream inputStream) { + try { + assertTrue(errmsg, doesStreamEqualStream(expectedInputStream, inputStream)); + } catch (IOException e) { + fail("Error reading from stream: " + e.getMessage()); + } + } + + /** + * Asserts that the contents of the two files are same. + * + * @param expected + * expected file. + * @param actual + * actual file. + */ + public static void assertFileEqualsFile(File expected, File actual) { + if (expected == null || !expected.exists()) + fail("Expected file doesn't exist"); + if (actual == null || !actual.exists()) + fail("Actual file doesn't exist"); + + final long expectedFileLen = expected.length(); + final long fileLen = actual.length(); + Assert.assertTrue("expectedFileLen=" + expectedFileLen + ", fileLen=" + fileLen + ", expectedFile=" + expected + + ", file=" + actual, expectedFileLen == fileLen); + try { + assertStreamEqualsStream("expected file: " + expected + " vs. actual file: " + actual, + new FileInputStream(expected), new FileInputStream(actual)); + } catch (Exception e) { + fail("Unable to compare files: " + e.getMessage()); + } + } + + /** + * Asserts that the contents in the specified string are exactly equal to the contents read from + * the specified input stream. The input stream will be closed at the end of this method. If any + * problems are encountered, or the stream's contents don't match up exactly with the string's + * contents, then this method will fail the current test. + * + * @param expected + * The string containing the expected data. + * @param actual + * The stream that will be read, compared to the expected string data, and finally + * closed. + */ + public static void assertStringEqualsStream(String expected, InputStream actual) { + try { + InputStream expectedInputStream = new ByteArrayInputStream(expected.getBytes()); + assertTrue(doesStreamEqualStream(expectedInputStream, actual)); + } catch (IOException e) { + fail("Error reading from stream: " + e.getMessage()); + } + } + + /** + * Returns true if, and only if, the contents read from the specified input streams are exactly + * equal. Both input streams will be closed at the end of this method. + * + * @param expected + * The input stream containing the expected contents. + * @param inputStream + * The stream that will be read, compared to the expected file contents, and finally + * closed. + * @return True if the two input streams contain the same data. + * @throws IOException + * If any problems are encountered comparing the file and stream. + */ + public static boolean doesStreamEqualStream(InputStream expected, InputStream actual) throws IOException { + + try { + final byte[] expectedDigest = InputStreamUtils.calculateMD5Digest(expected); + final byte[] actualDigest = InputStreamUtils.calculateMD5Digest(actual); + + return Arrays.equals(expectedDigest, actualDigest); + } catch (NoSuchAlgorithmException nse) { + throw SdkClientException.create(nse.getMessage(), nse); + } finally { + IoUtils.closeQuietly(expected, null); + IoUtils.closeQuietly(actual, null); + } + } + + /** + * Returns true if, and only if, the contents in the specified file are exactly equal to the + * contents read from the specified input stream. The input stream will be closed at the end of + * this method. + * + * @param expectedFile + * The file containing the expected contents. + * @param inputStream + * The stream that will be read, compared to the expected file contents, and finally + * closed. + * @throws IOException + * If any problems are encountered comparing the file and stream. + */ + public static boolean doesFileEqualStream(File expectedFile, InputStream inputStream) throws IOException { + InputStream expectedInputStream = new FileInputStream(expectedFile); + return doesStreamEqualStream(expectedInputStream, inputStream); + } + + /** + * Asserts that the specified AwsServiceException is valid, meaning it has a non-empty, + * non-null value for its message, requestId, etc. + * + * @param e + * The exception to validate. + */ + public static void assertValidException(AwsServiceException e) { + assertNotNull(e.requestId()); + assertTrue(e.requestId().trim().length() > 0); + + assertNotNull(e.getMessage()); + assertTrue(e.getMessage().trim().length() > 0); + + assertNotNull(e.awsErrorDetails().errorCode()); + assertTrue(e.awsErrorDetails().errorCode().trim().length() > 0); + + assertNotNull(e.awsErrorDetails().serviceName()); + assertTrue(e.awsErrorDetails().serviceName().startsWith("Amazon") + || e.awsErrorDetails().serviceName().startsWith("AWS")); + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/test/util/UnorderedCollectionComparator.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/test/util/UnorderedCollectionComparator.java new file mode 100644 index 000000000000..6aa370c677a6 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/test/util/UnorderedCollectionComparator.java @@ -0,0 +1,95 @@ +/* + * 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.mapper.dynamodb.test.util; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +/** + * This class includes some utility methods for comparing two unordered + * collections. + */ +public class UnorderedCollectionComparator { + + /** + * Compares two unordered lists of the same type. + */ + public static boolean equalUnorderedCollections( + Collection colA, Collection colB) { + return equalUnorderedCollections(colA, colB, new CrossTypeComparator() { + + @Override + public boolean equals(T a, T b) { + return (a == null && b == null) + || a.equals(b); + } + + }); + } + + /** + * Compares two unordered lists of different types, using the specified + * cross-type comparator. Null collections are treated as empty ones. + * Naively implemented using N(n^2) algorithm. + */ + public static boolean equalUnorderedCollections( + Collection colA, Collection colB, + final CrossTypeComparator comparator) { + if (colA == null || colB == null){ + if ( (colA == null || colA.isEmpty()) + && (colB == null || colB.isEmpty()) ) { + return true; + } else { + return false; + } + } + + // Add all elements into sets to remove duplicates. + Set setA = new HashSet(); + setA.addAll(colA); + Set setB = new HashSet(); + setB.addAll(colB); + + if ( setA.size() != setB.size() ){ + return false; + } + + for (A a : setA) { + boolean foundA = false; + for (B b : setB) { + if (comparator.equals(a, b)) { + foundA = true; + break; + } + } + if ( !foundA ) { + return false; + } + } + return true; + } + + /** + * A simple interface that attempts to compare objects of two different types + */ + public static interface CrossTypeComparator { + /** + * @return True if a and b should be treated as equal. + */ + public boolean equals(A a, B b); + } +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/utils/AttributeAdder.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/utils/AttributeAdder.java new file mode 100644 index 000000000000..a1cc3ca0613d --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/utils/AttributeAdder.java @@ -0,0 +1,48 @@ +/* + * 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.mapper.dynamodb.utils; + +import software.amazon.awssdk.mapper.dynamodb.AttributeTransformer; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +import java.util.HashMap; +import java.util.Map; + +public class AttributeAdder implements AttributeTransformer { + + private int start; + private final String additionalAttribute; + + public AttributeAdder(String attributeName, int start) { + this.additionalAttribute = attributeName; + this.start = start; + } + + @Override + public Map transform(final Parameters parameters) { + Map rval = new HashMap(parameters.getAttributeValues()); + rval.put(additionalAttribute, AttributeValue.builder().n(String.valueOf(start++)).build()); + return rval; + } + + @Override + public Map untransform(final Parameters parameters) { + Map rval = new HashMap(parameters.getAttributeValues()); + rval.remove(additionalAttribute); + return rval; + } + +} diff --git a/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/utils/NullAttributeAdder.java b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/utils/NullAttributeAdder.java new file mode 100644 index 000000000000..f7f89a44af56 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/java/software/amazon/awssdk/mapper/dynamodb/utils/NullAttributeAdder.java @@ -0,0 +1,45 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.mapper.dynamodb.utils; + +import software.amazon.awssdk.mapper.dynamodb.AttributeTransformer; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +import java.util.HashMap; +import java.util.Map; + +public class NullAttributeAdder implements AttributeTransformer { + + private final String additionalAttribute; + + public NullAttributeAdder(String attributeName) { + this.additionalAttribute = attributeName; + } + + @Override + public Map transform(final Parameters parameters) { + Map rval = new HashMap(parameters.getAttributeValues()); + rval.put(additionalAttribute, null); + return rval; + } + + @Override + public Map untransform(final Parameters parameters) { + Map rval = new HashMap(parameters.getAttributeValues()); + rval.remove(additionalAttribute); + return rval; + } +} diff --git a/services-custom/dynamodb-mapper/src/test/resources/batch_get_item_fixture.json b/services-custom/dynamodb-mapper/src/test/resources/batch_get_item_fixture.json new file mode 100644 index 000000000000..c1dfa634c5c5 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/resources/batch_get_item_fixture.json @@ -0,0 +1,3 @@ +{ + "two-keys" : "DynamoDB_20120810.BatchGetItem\n{\"RequestItems\":{\"M_String\":{\"Keys\":[{\"id\":{\"S\":\"a\"}},{\"id\":{\"S\":\"b\"}}],\"ConsistentRead\":false}}}" +} \ No newline at end of file diff --git a/services-custom/dynamodb-mapper/src/test/resources/batch_write_item_fixture.json b/services-custom/dynamodb-mapper/src/test/resources/batch_write_item_fixture.json new file mode 100644 index 000000000000..3dfd5f5d5a99 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/resources/batch_write_item_fixture.json @@ -0,0 +1,4 @@ +{ + "mixed-put-delete" : "DynamoDB_20120810.BatchWriteItem\n{\"RequestItems\":{\"M_String\":[{\"PutRequest\":{\"Item\":{\"id\":{\"S\":\"k\"},\"value\":{\"S\":\"p\"}}}},{\"DeleteRequest\":{\"Key\":{\"id\":{\"S\":\"d\"}}}}]}}", + "two-puts" : "DynamoDB_20120810.BatchWriteItem\n{\"RequestItems\":{\"M_String\":[{\"PutRequest\":{\"Item\":{\"id\":{\"S\":\"k\"},\"value\":{\"S\":\"a\"}}}},{\"PutRequest\":{\"Item\":{\"id\":{\"S\":\"k\"},\"value\":{\"S\":\"b\"}}}}]}}" +} \ No newline at end of file diff --git a/services-custom/dynamodb-mapper/src/test/resources/delete_item_fixture.json b/services-custom/dynamodb-mapper/src/test/resources/delete_item_fixture.json new file mode 100644 index 000000000000..9f1c03e15a1a --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/resources/delete_item_fixture.json @@ -0,0 +1,6 @@ +{ + "delete-with-condition-expression" : "DynamoDB_20120810.DeleteItem\n{\"TableName\":\"M_String\",\"Key\":{\"id\":{\"S\":\"k\"}},\"ConditionExpression\":\"attribute_exists(#v)\",\"ExpressionAttributeNames\":{\"#v\":\"value\"}}", + "delete-with-expected-expression" : "DynamoDB_20120810.DeleteItem\n{\"TableName\":\"M_String\",\"Key\":{\"id\":{\"S\":\"k\"}},\"Expected\":{\"value\":{\"Value\":{\"S\":\"hello\"}}}}", + "simple" : "DynamoDB_20120810.DeleteItem\n{\"TableName\":\"M_String\",\"Key\":{\"id\":{\"S\":\"k\"}},\"Expected\":{}}", + "versioned" : "DynamoDB_20120810.DeleteItem\n{\"TableName\":\"M_Versioned\",\"Key\":{\"id\":{\"S\":\"k\"}},\"Expected\":{\"version\":{\"Value\":{\"N\":\"3\"},\"Exists\":true}}}" +} \ No newline at end of file diff --git a/services-custom/dynamodb-mapper/src/test/resources/get_item_fixture.json b/services-custom/dynamodb-mapper/src/test/resources/get_item_fixture.json new file mode 100644 index 000000000000..b9b75c993be9 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/resources/get_item_fixture.json @@ -0,0 +1,4 @@ +{ + "by-key" : "DynamoDB_20120810.GetItem\n{\"TableName\":\"M_String\",\"Key\":{\"id\":{\"S\":\"k\"}},\"ConsistentRead\":false}", + "composite-key" : "DynamoDB_20120810.GetItem\n{\"TableName\":\"M_Range\",\"Key\":{\"id\":{\"S\":\"k\"},\"range\":{\"S\":\"r\"}},\"ConsistentRead\":false}" +} \ No newline at end of file diff --git a/services-custom/dynamodb-mapper/src/test/resources/log4j2.xml b/services-custom/dynamodb-mapper/src/test/resources/log4j2.xml new file mode 100644 index 000000000000..ec0cd8cd15f7 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/resources/log4j2.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + diff --git a/services-custom/dynamodb-mapper/src/test/resources/put_item_fixture.json b/services-custom/dynamodb-mapper/src/test/resources/put_item_fixture.json new file mode 100644 index 000000000000..1b1f236736c6 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/resources/put_item_fixture.json @@ -0,0 +1,32 @@ +{ + "bool-native/false" : "DynamoDB_20120810.PutItem\n{\"TableName\":\"M_AllTypes\",\"Item\":{\"id\":{\"S\":\"k\"},\"nativeBool\":{\"BOOL\":false}}}", + "bool-native/true" : "DynamoDB_20120810.PutItem\n{\"TableName\":\"M_AllTypes\",\"Item\":{\"id\":{\"S\":\"k\"},\"nativeBool\":{\"BOOL\":true}}}", + "bool-numeric/false" : "DynamoDB_20120810.PutItem\n{\"TableName\":\"M_AllTypes\",\"Item\":{\"numericBool\":{\"N\":\"0\"},\"id\":{\"S\":\"k\"}}}", + "bool-numeric/true" : "DynamoDB_20120810.PutItem\n{\"TableName\":\"M_AllTypes\",\"Item\":{\"numericBool\":{\"N\":\"1\"},\"id\":{\"S\":\"k\"}}}", + "clobber-suppresses-version-check" : "DynamoDB_20120810.PutItem\n{\"TableName\":\"M_Versioned\",\"Item\":{\"id\":{\"S\":\"k\"},\"version\":{\"N\":\"1\"}}}", + "date-iso8601" : "DynamoDB_20120810.PutItem\n{\"TableName\":\"M_Dated\",\"Item\":{\"createdAt\":{\"S\":\"2020-09-13T12:26:40.000Z\"},\"id\":{\"S\":\"k\"}}}", + "document/populated" : "DynamoDB_20120810.PutItem\n{\"TableName\":\"M_AllTypes\",\"Item\":{\"document\":{\"M\":{\"city\":{\"S\":\"Seattle\"}}},\"id\":{\"S\":\"k\"}}}", + "double/fractional" : "DynamoDB_20120810.PutItem\n{\"TableName\":\"M_AllTypes\",\"Item\":{\"double\":{\"N\":\"1.5\"},\"id\":{\"S\":\"k\"}}}", + "double/negative" : "DynamoDB_20120810.PutItem\n{\"TableName\":\"M_AllTypes\",\"Item\":{\"double\":{\"N\":\"-0.25\"},\"id\":{\"S\":\"k\"}}}", + "double/whole" : "DynamoDB_20120810.PutItem\n{\"TableName\":\"M_AllTypes\",\"Item\":{\"double\":{\"N\":\"1.0\"},\"id\":{\"S\":\"k\"}}}", + "enum/value" : "DynamoDB_20120810.PutItem\n{\"TableName\":\"M_AllTypes\",\"Item\":{\"id\":{\"S\":\"k\"},\"enum\":{\"S\":\"GREEN\"}}}", + "flattened" : "DynamoDB_20120810.PutItem\n{\"TableName\":\"M_Flattened\",\"Item\":{\"firstName\":{\"S\":\"Ada\"},\"id\":{\"S\":\"k\"}}}", + "inherited-key" : "DynamoDB_20120810.PutItem\n{\"TableName\":\"M_Inherited\",\"Item\":{\"label\":{\"S\":\"tag\"},\"id\":{\"S\":\"k\"}}}", + "list/multi" : "DynamoDB_20120810.PutItem\n{\"TableName\":\"M_AllTypes\",\"Item\":{\"id\":{\"S\":\"k\"},\"list\":{\"L\":[{\"S\":\"first\"},{\"S\":\"second\"},{\"S\":\"third\"}]}}}", + "list/single" : "DynamoDB_20120810.PutItem\n{\"TableName\":\"M_AllTypes\",\"Item\":{\"id\":{\"S\":\"k\"},\"list\":{\"L\":[{\"S\":\"only\"}]}}}", + "map/multi" : "DynamoDB_20120810.PutItem\n{\"TableName\":\"M_AllTypes\",\"Item\":{\"id\":{\"S\":\"k\"},\"map\":{\"M\":{\"b\":{\"S\":\"2\"},\"a\":{\"S\":\"1\"}}}}}", + "map/single" : "DynamoDB_20120810.PutItem\n{\"TableName\":\"M_AllTypes\",\"Item\":{\"id\":{\"S\":\"k\"},\"map\":{\"M\":{\"k\":{\"S\":\"v\"}}}}}", + "number-set/multi" : "DynamoDB_20120810.PutItem\n{\"TableName\":\"M_AllTypes\",\"Item\":{\"id\":{\"S\":\"k\"},\"numberSet\":{\"NS\":[\"3\",\"1\",\"2\"]}}}", + "number-set/single" : "DynamoDB_20120810.PutItem\n{\"TableName\":\"M_AllTypes\",\"Item\":{\"id\":{\"S\":\"k\"},\"numberSet\":{\"NS\":[\"1\"]}}}", + "number/max-long" : "DynamoDB_20120810.PutItem\n{\"TableName\":\"M_AllTypes\",\"Item\":{\"number\":{\"N\":\"9223372036854775807\"},\"id\":{\"S\":\"k\"}}}", + "number/min-long" : "DynamoDB_20120810.PutItem\n{\"TableName\":\"M_AllTypes\",\"Item\":{\"number\":{\"N\":\"-9223372036854775808\"},\"id\":{\"S\":\"k\"}}}", + "number/negative" : "DynamoDB_20120810.PutItem\n{\"TableName\":\"M_AllTypes\",\"Item\":{\"number\":{\"N\":\"-7\"},\"id\":{\"S\":\"k\"}}}", + "number/positive" : "DynamoDB_20120810.PutItem\n{\"TableName\":\"M_AllTypes\",\"Item\":{\"number\":{\"N\":\"42\"},\"id\":{\"S\":\"k\"}}}", + "number/zero" : "DynamoDB_20120810.PutItem\n{\"TableName\":\"M_AllTypes\",\"Item\":{\"number\":{\"N\":\"0\"},\"id\":{\"S\":\"k\"}}}", + "put-keeps-version-check" : "DynamoDB_20120810.PutItem\n{\"TableName\":\"M_Versioned\",\"Item\":{\"id\":{\"S\":\"k\"},\"version\":{\"N\":\"1\"}},\"Expected\":{\"version\":{\"Exists\":false}}}", + "string-set/multi" : "DynamoDB_20120810.PutItem\n{\"TableName\":\"M_AllTypes\",\"Item\":{\"stringSet\":{\"SS\":[\"c\",\"a\",\"b\"]},\"id\":{\"S\":\"k\"}}}", + "string-set/single" : "DynamoDB_20120810.PutItem\n{\"TableName\":\"M_AllTypes\",\"Item\":{\"stringSet\":{\"SS\":[\"a\"]},\"id\":{\"S\":\"k\"}}}", + "string/normal" : "DynamoDB_20120810.PutItem\n{\"TableName\":\"M_String\",\"Item\":{\"id\":{\"S\":\"k\"},\"value\":{\"S\":\"hello\"}}}", + "string/unicode" : "DynamoDB_20120810.PutItem\n{\"TableName\":\"M_String\",\"Item\":{\"id\":{\"S\":\"k\"},\"value\":{\"S\":\"héllo-世界-\\uD83D\\uDE00\"}}}", + "string/whitespace" : "DynamoDB_20120810.PutItem\n{\"TableName\":\"M_String\",\"Item\":{\"id\":{\"S\":\"k\"},\"value\":{\"S\":\" pad \\t\\n\"}}}" +} \ No newline at end of file diff --git a/services-custom/dynamodb-mapper/src/test/resources/query_fixture.json b/services-custom/dynamodb-mapper/src/test/resources/query_fixture.json new file mode 100644 index 000000000000..24fa8a521ecb --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/resources/query_fixture.json @@ -0,0 +1,9 @@ +{ + "consistent-read-select" : "DynamoDB_20120810.Query\n{\"TableName\":\"M_String\",\"Select\":\"ALL_ATTRIBUTES\",\"ConsistentRead\":true,\"KeyConditions\":{\"id\":{\"AttributeValueList\":[{\"S\":\"k\"}],\"ComparisonOperator\":\"EQ\"}},\"ScanIndexForward\":true}", + "exclusive-start-key" : "DynamoDB_20120810.Query\n{\"TableName\":\"M_String\",\"ConsistentRead\":true,\"KeyConditions\":{\"id\":{\"AttributeValueList\":[{\"S\":\"k\"}],\"ComparisonOperator\":\"EQ\"}},\"ScanIndexForward\":true,\"ExclusiveStartKey\":{\"id\":{\"S\":\"k\"}}}", + "hash-key-equals" : "DynamoDB_20120810.Query\n{\"TableName\":\"M_String\",\"ConsistentRead\":true,\"KeyConditions\":{\"id\":{\"AttributeValueList\":[{\"S\":\"k\"}],\"ComparisonOperator\":\"EQ\"}},\"ScanIndexForward\":true}", + "key-condition-expression" : "DynamoDB_20120810.Query\n{\"TableName\":\"M_String\",\"ConsistentRead\":true,\"ScanIndexForward\":true,\"KeyConditionExpression\":\"#i = :v\",\"ExpressionAttributeNames\":{\"#i\":\"id\"},\"ExpressionAttributeValues\":{\":v\":{\"S\":\"k\"}}}", + "projection-expression" : "DynamoDB_20120810.Query\n{\"TableName\":\"M_String\",\"ConsistentRead\":true,\"KeyConditions\":{\"id\":{\"AttributeValueList\":[{\"S\":\"k\"}],\"ComparisonOperator\":\"EQ\"}},\"ScanIndexForward\":true,\"ProjectionExpression\":\"#v\",\"ExpressionAttributeNames\":{\"#v\":\"value\"}}", + "query-filter" : "DynamoDB_20120810.Query\n{\"TableName\":\"M_String\",\"ConsistentRead\":true,\"KeyConditions\":{\"id\":{\"AttributeValueList\":[{\"S\":\"k\"}],\"ComparisonOperator\":\"EQ\"}},\"QueryFilter\":{\"value\":{\"AttributeValueList\":[{\"S\":\"hello\"}],\"ComparisonOperator\":\"EQ\"}},\"ScanIndexForward\":true}", + "range-condition-limit-desc" : "DynamoDB_20120810.Query\n{\"TableName\":\"M_Range\",\"Limit\":10,\"ConsistentRead\":true,\"KeyConditions\":{\"range\":{\"AttributeValueList\":[{\"N\":\"5\"}],\"ComparisonOperator\":\"GT\"},\"id\":{\"AttributeValueList\":[{\"S\":\"k\"}],\"ComparisonOperator\":\"EQ\"}},\"ScanIndexForward\":false}" +} \ No newline at end of file diff --git a/services-custom/dynamodb-mapper/src/test/resources/scan_fixture.json b/services-custom/dynamodb-mapper/src/test/resources/scan_fixture.json new file mode 100644 index 000000000000..cfaee2ce4260 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/resources/scan_fixture.json @@ -0,0 +1,8 @@ +{ + "consistent-read" : "DynamoDB_20120810.Scan\n{\"TableName\":\"M_String\",\"ConsistentRead\":true}", + "filter-expression" : "DynamoDB_20120810.Scan\n{\"TableName\":\"M_String\",\"FilterExpression\":\"#v = :v\",\"ExpressionAttributeNames\":{\"#v\":\"value\"},\"ExpressionAttributeValues\":{\":v\":{\"S\":\"hello\"}}}", + "filtered-limited" : "DynamoDB_20120810.Scan\n{\"TableName\":\"M_String\",\"Limit\":25,\"ScanFilter\":{\"value\":{\"AttributeValueList\":[{\"S\":\"hello\"}],\"ComparisonOperator\":\"EQ\"}}}", + "projection-expression" : "DynamoDB_20120810.Scan\n{\"TableName\":\"M_String\",\"ProjectionExpression\":\"#v\",\"ExpressionAttributeNames\":{\"#v\":\"value\"}}", + "segment" : "DynamoDB_20120810.Scan\n{\"TableName\":\"M_String\",\"TotalSegments\":4,\"Segment\":1}", + "unfiltered" : "DynamoDB_20120810.Scan\n{\"TableName\":\"M_String\"}" +} \ No newline at end of file diff --git a/services-custom/dynamodb-mapper/src/test/resources/transaction_load_fixture.json b/services-custom/dynamodb-mapper/src/test/resources/transaction_load_fixture.json new file mode 100644 index 000000000000..4b9c93ccce19 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/resources/transaction_load_fixture.json @@ -0,0 +1,3 @@ +{ + "two-loads" : "DynamoDB_20120810.TransactGetItems\n{\"TransactItems\":[{\"Get\":{\"Key\":{\"id\":{\"S\":\"a\"}},\"TableName\":\"M_String\"}},{\"Get\":{\"Key\":{\"id\":{\"S\":\"b\"}},\"TableName\":\"M_String\"}}]}" +} \ No newline at end of file diff --git a/services-custom/dynamodb-mapper/src/test/resources/transaction_write_fixture.json b/services-custom/dynamodb-mapper/src/test/resources/transaction_write_fixture.json new file mode 100644 index 000000000000..aa716bac9a6d --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/resources/transaction_write_fixture.json @@ -0,0 +1,5 @@ +{ + "condition-check" : "DynamoDB_20120810.TransactWriteItems\n{\"TransactItems\":[{\"ConditionCheck\":{\"Key\":{\"id\":{\"S\":\"c\"}},\"TableName\":\"M_String\",\"ConditionExpression\":\"attribute_exists(#i)\",\"ExpressionAttributeNames\":{\"#i\":\"id\"}}}],\"ClientRequestToken\":\"fixed-token\"}", + "put-update-delete" : "DynamoDB_20120810.TransactWriteItems\n{\"TransactItems\":[{\"Put\":{\"Item\":{\"id\":{\"S\":\"k\"},\"value\":{\"S\":\"p\"}},\"TableName\":\"M_String\"}},{\"Update\":{\"Key\":{\"id\":{\"S\":\"k\"}},\"UpdateExpression\":\"SET #68a00 = :68a00\",\"TableName\":\"M_String\",\"ExpressionAttributeNames\":{\"#68a00\":\"value\"},\"ExpressionAttributeValues\":{\":68a00\":{\"S\":\"u\"}}}},{\"Delete\":{\"Key\":{\"id\":{\"S\":\"d\"}},\"TableName\":\"M_String\"}}],\"ClientRequestToken\":\"fixed-token\"}", + "versioned-auto-condition" : "DynamoDB_20120810.TransactWriteItems\n{\"TransactItems\":[{\"Put\":{\"Item\":{\"id\":{\"S\":\"k\"},\"version\":{\"N\":\"1\"}},\"TableName\":\"M_Versioned\",\"ConditionExpression\":\"attribute_not_exists(#versionAttributeName1)\",\"ExpressionAttributeNames\":{\"#versionAttributeName1\":\"version\"}}}],\"ClientRequestToken\":\"fixed-token\"}" +} \ No newline at end of file diff --git a/services-custom/dynamodb-mapper/src/test/resources/unmarshall_item_fixture.json b/services-custom/dynamodb-mapper/src/test/resources/unmarshall_item_fixture.json new file mode 100644 index 000000000000..88122efe623a --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/resources/unmarshall_item_fixture.json @@ -0,0 +1,17 @@ +{ + "bool-native-false" : "{\n \"id\" : \"k\",\n \"nativeBool\" : false\n}", + "bool-native-true" : "{\n \"id\" : \"k\",\n \"nativeBool\" : true\n}", + "bool-numeric-false" : "{\n \"id\" : \"k\",\n \"numericBool\" : false\n}", + "bool-numeric-true" : "{\n \"id\" : \"k\",\n \"numericBool\" : true\n}", + "document" : "{\n \"document\" : {\n \"city\" : \"Seattle\",\n \"zip\" : 98101\n },\n \"id\" : \"k\"\n}", + "double" : "{\n \"doubleValue\" : 1.5,\n \"id\" : \"k\"\n}", + "enum" : "{\n \"enumValue\" : \"GREEN\",\n \"id\" : \"k\"\n}", + "list" : "{\n \"id\" : \"k\",\n \"list\" : [ \"first\", \"second\" ]\n}", + "map" : "{\n \"id\" : \"k\",\n \"map\" : {\n \"a\" : \"1\",\n \"b\" : \"2\"\n }\n}", + "number" : "{\n \"id\" : \"k\",\n \"number\" : 42\n}", + "number-negative" : "{\n \"id\" : \"k\",\n \"number\" : -7\n}", + "number-set" : "{\n \"id\" : \"k\",\n \"numberSet\" : [ 1, 2, 3 ]\n}", + "string" : "{\n \"id\" : \"k\",\n \"value\" : \"hello\"\n}", + "string-set" : "{\n \"id\" : \"k\",\n \"stringSet\" : [ \"a\", \"b\", \"c\" ]\n}", + "string-unicode" : "{\n \"id\" : \"k\",\n \"value\" : \"héllo-世界-\uD83D\uDE00\"\n}" +} \ No newline at end of file diff --git a/services-custom/dynamodb-mapper/src/test/resources/update_item_fixture.json b/services-custom/dynamodb-mapper/src/test/resources/update_item_fixture.json new file mode 100644 index 000000000000..9ac22408e896 --- /dev/null +++ b/services-custom/dynamodb-mapper/src/test/resources/update_item_fixture.json @@ -0,0 +1,8 @@ +{ + "append-set-behavior" : "DynamoDB_20120810.UpdateItem\n{\"TableName\":\"M_AllTypes\",\"Key\":{\"id\":{\"S\":\"k\"}},\"AttributeUpdates\":{\"stringSet\":{\"Value\":{\"SS\":[\"a\",\"b\"]},\"Action\":\"ADD\"}},\"ReturnValues\":\"ALL_NEW\"}", + "null-attr-delete" : "DynamoDB_20120810.UpdateItem\n{\"TableName\":\"M_String\",\"Key\":{\"id\":{\"S\":\"k\"}},\"AttributeUpdates\":{\"value\":{\"Action\":\"DELETE\"}},\"ReturnValues\":\"ALL_NEW\"}", + "null-attr-skip" : "DynamoDB_20120810.UpdateItem\n{\"TableName\":\"M_String\",\"Key\":{\"id\":{\"S\":\"k\"}},\"AttributeUpdates\":{},\"ReturnValues\":\"ALL_NEW\"}", + "save-default-update" : "DynamoDB_20120810.UpdateItem\n{\"TableName\":\"M_String\",\"Key\":{\"id\":{\"S\":\"k\"}},\"AttributeUpdates\":{\"value\":{\"Value\":{\"S\":\"hello\"},\"Action\":\"PUT\"}},\"ReturnValues\":\"ALL_NEW\"}", + "save-with-expected-expression" : "DynamoDB_20120810.UpdateItem\n{\"TableName\":\"M_String\",\"Key\":{\"id\":{\"S\":\"k\"}},\"AttributeUpdates\":{\"value\":{\"Value\":{\"S\":\"hello\"},\"Action\":\"PUT\"}},\"Expected\":{\"value\":{\"Exists\":false}},\"ReturnValues\":\"ALL_NEW\"}", + "versioned-first-save" : "DynamoDB_20120810.UpdateItem\n{\"TableName\":\"M_Versioned\",\"Key\":{\"id\":{\"S\":\"k\"}},\"AttributeUpdates\":{\"version\":{\"Value\":{\"N\":\"1\"},\"Action\":\"PUT\"}},\"Expected\":{\"version\":{\"Exists\":false}},\"ReturnValues\":\"ALL_NEW\"}" +} \ No newline at end of file diff --git a/services-custom/pom.xml b/services-custom/pom.xml index d894855a6249..62b6efe752d5 100644 --- a/services-custom/pom.xml +++ b/services-custom/pom.xml @@ -29,6 +29,7 @@ dynamodb-enhanced + dynamodb-mapper s3-transfer-manager iam-policy-builder s3-event-notifications diff --git a/test/architecture-tests/pom.xml b/test/architecture-tests/pom.xml index 0fd8e8afa96e..88d09f671a3d 100644 --- a/test/architecture-tests/pom.xml +++ b/test/architecture-tests/pom.xml @@ -46,6 +46,11 @@ software.amazon.awssdk ${awsjavasdk.version} + + dynamodb-mapper + software.amazon.awssdk + ${awsjavasdk.version} + cloudwatch-metric-publisher software.amazon.awssdk diff --git a/test/architecture-tests/src/test/java/software/amazon/awssdk/archtests/ArchUtils.java b/test/architecture-tests/src/test/java/software/amazon/awssdk/archtests/ArchUtils.java index acae7b3b4a0e..2c48e2c33eae 100644 --- a/test/architecture-tests/src/test/java/software/amazon/awssdk/archtests/ArchUtils.java +++ b/test/architecture-tests/src/test/java/software/amazon/awssdk/archtests/ArchUtils.java @@ -35,6 +35,11 @@ public static Pattern classNameToPattern(String className) { return Pattern.compile(".*/" + className.replace('.', '/') + ".class"); } + public static Pattern classWithInnerClassesToPattern(String className) { + // inner or inline/anonymous classes have $ followed by a name or number eg "$Inner" or "$1" + return Pattern.compile(".*/" + className.replace('.', '/') + "(\\$.*)?.class"); + } + public static boolean resideInSameRootPackage(String pkg1, String pkg2) { if (pkg1.startsWith(pkg2) || pkg2.startsWith(pkg1)) { return true; diff --git a/test/architecture-tests/src/test/java/software/amazon/awssdk/archtests/CodingConventionWithSuppressionTest.java b/test/architecture-tests/src/test/java/software/amazon/awssdk/archtests/CodingConventionWithSuppressionTest.java index a7867216a65f..feae8cdf8b92 100644 --- a/test/architecture-tests/src/test/java/software/amazon/awssdk/archtests/CodingConventionWithSuppressionTest.java +++ b/test/architecture-tests/src/test/java/software/amazon/awssdk/archtests/CodingConventionWithSuppressionTest.java @@ -60,7 +60,12 @@ public class CodingConventionWithSuppressionTest { ArchUtils.classNameToPattern(RetryableSubAsyncRequestBody.class), ArchUtils.classNameToPattern(KnownContentLengthAsyncRequestBodySubscriber.class), ArchUtils.classNameToPattern(UnknownContentLengthAsyncRequestBodySubscriber.class), - ArchUtils.classNameToPattern(CopyObjectHelper.class))); + ArchUtils.classNameToPattern(CopyObjectHelper.class), + ArchUtils.classNameToPattern("software.amazon.awssdk.mapper.dynamodb.ConversionSchemas"), + ArchUtils.classNameToPattern( + "software.amazon.awssdk.mapper.dynamodb.internal.marshallers.ObjectSetToStringSetMarshaller"), + ArchUtils.classWithInnerClassesToPattern( + "software.amazon.awssdk.mapper.dynamodb.StandardModelFactories"))); private static final Set ALLOWED_ERROR_LOG_SUPPRESSION = new HashSet<>( Arrays.asList( diff --git a/test/tests-coverage-reporting/pom.xml b/test/tests-coverage-reporting/pom.xml index f68687c15209..f351f0cc8b9c 100644 --- a/test/tests-coverage-reporting/pom.xml +++ b/test/tests-coverage-reporting/pom.xml @@ -227,6 +227,11 @@ software.amazon.awssdk ${awsjavasdk.version} + + dynamodb-mapper + software.amazon.awssdk + ${awsjavasdk.version} + cloudwatch-metric-publisher software.amazon.awssdk