diff --git a/pom.xml b/pom.xml index 6a8d8b09fed0..8eaf3dd698a7 100644 --- a/pom.xml +++ b/pom.xml @@ -79,6 +79,7 @@ test/service-test-utils test/test-utils test/codegen-generated-classes-test + test/dynamodb-mapper-v2 test/sdk-benchmarks test/http-client-benchmarks test/module-path-tests diff --git a/test/dynamodb-mapper-v2/pom.xml b/test/dynamodb-mapper-v2/pom.xml new file mode 100644 index 000000000000..419f2152e72f --- /dev/null +++ b/test/dynamodb-mapper-v2/pom.xml @@ -0,0 +1,114 @@ + + + 4.0.0 + + software.amazon.awssdk + dynamodb-mapper-v2 + 1.0.0-SNAPSHOT + jar + + DynamoDB Mapper for AWS SDK v2 + Port of the v1 DynamoDBMapper to work with the v2 DynamoDbClient + + + 1.8 + 1.8 + UTF-8 + 2.31.9 + + + + + software.amazon.awssdk + dynamodb + ${awssdk.version} + + + org.slf4j + slf4j-api + 1.7.36 + + + + joda-time + joda-time + 2.12.5 + true + + + + + junit + junit + 4.13.2 + test + + + org.hamcrest + hamcrest + 2.2 + test + + + org.mockito + mockito-core + 4.11.0 + test + + + org.slf4j + slf4j-simple + 1.7.36 + test + + + com.amazonaws + DynamoDBLocal + 1.25.0 + test + + + com.almworks.sqlite4java + sqlite4java + 1.0.392 + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-native-libs + process-test-resources + + copy-dependencies + + + test + so,dll,dylib + ${project.build.directory}/native-libs + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + ${project.build.directory}/native-libs + + + 0 + + + + + + diff --git a/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/AbstractDynamoDBMapper.java b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/AbstractDynamoDBMapper.java new file mode 100644 index 000000000000..486ad9a95370 --- /dev/null +++ b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/AbstractDynamoDBMapper.java @@ -0,0 +1,101 @@ +/* + * Copyright 2015-2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://aws.amazon.com/apache2.0 + * + * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and + * limitations under the License. + */ +package software.amazon.awssdk.dynamodb.datamodeling; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import java.util.Map; + +/** + * Abstract implementation of {@code IDynamoDBMapper}. Stripped to load path for POC. + */ +public abstract 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()); + } +} diff --git a/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/AttributeTransformer.java b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/AttributeTransformer.java new file mode 100644 index 000000000000..4246b2c39a8c --- /dev/null +++ b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/AttributeTransformer.java @@ -0,0 +1,114 @@ +/* + * Copyright 2013-2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://aws.amazon.com/apache2.0 + * + * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and + * limitations under the License. + */ +package software.amazon.awssdk.dynamodb.datamodeling; + +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. + */ +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/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/BatchLoadContext.java b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/BatchLoadContext.java new file mode 100644 index 000000000000..faabf55a9e92 --- /dev/null +++ b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/BatchLoadContext.java @@ -0,0 +1,91 @@ +/* + * Copyright 2010-2025 Amazon.com, Inc. or its affiliates. All Rights + * Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.dynamodb.datamodeling; + +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 + */ +public class BatchLoadContext { + /** + * The BatchGetItemResponse returned by the DynamoDB client. + */ + private BatchGetItemResponse batchGetItemResult; + /** + * The BatchGetItemRequest. + */ + private final 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 = java.util.Objects.requireNonNull(batchGetItemRequest, "batchGetItemRequest"); + this.batchGetItemResult = null; + this.retriesAttempted = 0; + } + + /** + * @return the BatchGetItemResponse + */ + public BatchGetItemResponse getBatchGetItemResponse() { + return batchGetItemResult; + } + + /** + * @return the BatchGetItemResponse + */ + public void setBatchGetItemResponse(BatchGetItemResponse batchGetItemResult) { + this.batchGetItemResult = batchGetItemResult; + } + + + /** + * @return the BatchGetItemRequest. + */ + public BatchGetItemRequest getBatchGetItemRequest() { + return 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/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/ConversionSchema.java b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/ConversionSchema.java new file mode 100644 index 000000000000..12a37e5daea3 --- /dev/null +++ b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/ConversionSchema.java @@ -0,0 +1,64 @@ +/* + * Copyright 2014-2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://aws.amazon.com/apache2.0 + * + * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and + * limitations under the License. + */ +package software.amazon.awssdk.dynamodb.datamodeling; + +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. + */ +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/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/ConversionSchemas.java b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/ConversionSchemas.java new file mode 100644 index 000000000000..778464aeb984 --- /dev/null +++ b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/ConversionSchemas.java @@ -0,0 +1,83 @@ +/* + * Copyright 2016-2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://aws.amazon.com/apache2.0 + * + * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and + * limitations under the License. + */ +package software.amazon.awssdk.dynamodb.datamodeling; + +import software.amazon.awssdk.dynamodb.datamodeling.DynamoDBMapperFieldModel.DynamoDBAttributeType; +import software.amazon.awssdk.dynamodb.datamodeling.StandardModelFactories.Rule; +import software.amazon.awssdk.dynamodb.datamodeling.StandardModelFactories.RuleFactory; + +/** + * Pre-defined strategies for mapping between Java types and DynamoDB types. + */ +public final class ConversionSchemas { + + /** + * The V1 schema mapping, which retains strict backwards compatibility with + * the original DynamoDB data model. This is compatible with the + * {@link DynamoDBTyped} annotation. + */ + public static final ConversionSchema V1 = new NamedConversionSchema("V1ConversionSchema"); + + /** + * A V2 compatible conversion schema which is the default. Supports + * both V1 and V2 style annotations. + */ + public static final ConversionSchema V2_COMPATIBLE = new NamedConversionSchema("V2CompatibleConversionSchema"); + + /** + * The V2 schema mapping, which removes support for some legacy types. + */ + public static final ConversionSchema V2 = new NamedConversionSchema("V2ConversionSchema"); + + static final ConversionSchema DEFAULT = V2_COMPATIBLE; + + private static final class NamedConversionSchema implements ConversionSchema { + private final String name; + private NamedConversionSchema(String name) { + this.name = name; + } + @Override + public ItemConverter getConverter(Dependencies depends) { + throw new UnsupportedOperationException( + "Legacy ItemConverter not supported; use StandardModelFactories rules"); + } + @Override + public String toString() { + return name; + } + } + + /** + * Rule factory that wraps the standard type converter rules. + * For built-in schemas (V1, V2_COMPATIBLE, V2), delegates directly to the + * wrapped rules. Custom schemas are not supported in this port. + */ + static class ItemConverterRuleFactory implements RuleFactory { + private final RuleFactory typeConverters; + + ItemConverterRuleFactory(DynamoDBMapperConfig config, RuleFactory typeConverters) { + this.typeConverters = typeConverters; + } + + @Override + public Rule getRule(ConvertibleType type) { + return typeConverters.getRule(type); + } + } + + ConversionSchemas() { + throw new UnsupportedOperationException(); + } +} diff --git a/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/ConvertibleType.java b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/ConvertibleType.java new file mode 100644 index 000000000000..8a5cf2bf4d7d --- /dev/null +++ b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/ConvertibleType.java @@ -0,0 +1,214 @@ +/* + * Copyright 2016-2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://aws.amazon.com/apache2.0 + * + * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and + * limitations under the License. + */ +package software.amazon.awssdk.dynamodb.datamodeling; + +import software.amazon.awssdk.dynamodb.datamodeling.DynamoDBMapperFieldModel.DynamoDBAttributeType; +import software.amazon.awssdk.dynamodb.datamodeling.StandardAnnotationMaps.TypedMap; +import software.amazon.awssdk.dynamodb.datamodeling.StandardTypeConverters.Scalar; +import software.amazon.awssdk.dynamodb.datamodeling.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. + */ +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) { + final ConvertibleType target = ConvertibleType.of(typeConverter); + this.targetType = target.targetType; + this.params = target.params; + } else if (genericType instanceof ParameterizedType) { + final 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() { + final 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) { + final Class clazz = converter.getClass(); + if (!clazz.isInterface()) { + for (Class c = clazz; Object.class != c; c = c.getSuperclass()) { + for (final Type genericType : c.getGenericInterfaces()) { + final 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); + } + } + } + } + final 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) { + final 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; + } + final TypedMap annotations = StandardAnnotationMaps.of(targetType); + return new ConvertibleType(genericType, annotations, null); + } + +} diff --git a/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDB.java b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDB.java new file mode 100644 index 000000000000..6dbee2d7d887 --- /dev/null +++ b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDB.java @@ -0,0 +1,28 @@ +/* + * Copyright 2011-2025 Amazon Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://aws.amazon.com/apache2.0 + * + * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and + * limitations under the License. + */ +package software.amazon.awssdk.dynamodb.datamodeling; + +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/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBAttribute.java b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBAttribute.java new file mode 100644 index 000000000000..1fd111c93e7e --- /dev/null +++ b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBAttribute.java @@ -0,0 +1,58 @@ +/* + * Copyright 2011-2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://aws.amazon.com/apache2.0 + * + * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and + * limitations under the License. + */ +package software.amazon.awssdk.dynamodb.datamodeling; + +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 com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBFlattened + */ + String mappedBy() default ""; +} diff --git a/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBAutoGenerateStrategy.java b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBAutoGenerateStrategy.java new file mode 100644 index 000000000000..62a7cf3ff820 --- /dev/null +++ b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBAutoGenerateStrategy.java @@ -0,0 +1,33 @@ +/* + * Copyright 2010-2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package software.amazon.awssdk.dynamodb.datamodeling; + +/** + * 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/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBAutoGenerated.java b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBAutoGenerated.java new file mode 100644 index 000000000000..b0d6dc92e066 --- /dev/null +++ b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBAutoGenerated.java @@ -0,0 +1,82 @@ +/* + * Copyright 2011-2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://aws.amazon.com/apache2.0 + * + * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and + * limitations under the License. + */ +package software.amazon.awssdk.dynamodb.datamodeling; + +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/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBAutoGenerator.java b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBAutoGenerator.java new file mode 100644 index 000000000000..c0b496a7b89d --- /dev/null +++ b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBAutoGenerator.java @@ -0,0 +1,68 @@ +/* + * Copyright 2016-2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://aws.amazon.com/apache2.0 + * + * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and + * limitations under the License. + */ +package software.amazon.awssdk.dynamodb.datamodeling; + + +/** + * 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 com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBAutoGenerated + */ +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}. + */ + 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/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBDocument.java b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBDocument.java new file mode 100644 index 000000000000..5cb52764c35b --- /dev/null +++ b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBDocument.java @@ -0,0 +1,35 @@ +/* + * Copyright 2014-2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://aws.amazon.com/apache2.0 + * + * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and + * limitations under the License. + */ +package software.amazon.awssdk.dynamodb.datamodeling; + +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/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBFlattened.java b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBFlattened.java new file mode 100644 index 000000000000..80eaeb786e80 --- /dev/null +++ b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBFlattened.java @@ -0,0 +1,67 @@ +/* + * Copyright 2016-2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://aws.amazon.com/apache2.0 + * + * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and + * limitations under the License. + */ +package software.amazon.awssdk.dynamodb.datamodeling; + +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/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBHashKey.java b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBHashKey.java new file mode 100644 index 000000000000..23a447ac0a25 --- /dev/null +++ b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBHashKey.java @@ -0,0 +1,41 @@ +/* + * Copyright 2011-2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://aws.amazon.com/apache2.0 + * + * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and + * limitations under the License. + */ +package software.amazon.awssdk.dynamodb.datamodeling; + +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/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBIgnore.java b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBIgnore.java new file mode 100644 index 000000000000..37271c9d722d --- /dev/null +++ b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBIgnore.java @@ -0,0 +1,36 @@ +/* + * Copyright 2011-2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://aws.amazon.com/apache2.0 + * + * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and + * limitations under the License. + */ +package software.amazon.awssdk.dynamodb.datamodeling; + +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/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBIndexHashKey.java b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBIndexHashKey.java new file mode 100644 index 000000000000..ab0453cc484c --- /dev/null +++ b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBIndexHashKey.java @@ -0,0 +1,58 @@ +/* + * Copyright 2011-2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://aws.amazon.com/apache2.0 + * + * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and + * limitations under the License. + */ +package software.amazon.awssdk.dynamodb.datamodeling; + +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/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBIndexRangeKey.java b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBIndexRangeKey.java new file mode 100644 index 000000000000..8bb2a86eca3c --- /dev/null +++ b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBIndexRangeKey.java @@ -0,0 +1,75 @@ +/* + * Copyright 2011-2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://aws.amazon.com/apache2.0 + * + * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and + * limitations under the License. + */ +package software.amazon.awssdk.dynamodb.datamodeling; + +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/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBKeyed.java b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBKeyed.java new file mode 100644 index 000000000000..23ae15ee5336 --- /dev/null +++ b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBKeyed.java @@ -0,0 +1,47 @@ +/* + * Copyright 2016-2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://aws.amazon.com/apache2.0 + * + * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and + * limitations under the License. + */ +package software.amazon.awssdk.dynamodb.datamodeling; + +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/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBMapper.java b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBMapper.java new file mode 100644 index 000000000000..1f395b312cc8 --- /dev/null +++ b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBMapper.java @@ -0,0 +1,207 @@ +/* + * Copyright 2010-2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://aws.amazon.com/apache2.0 + * + * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and + * limitations under the License. + */ +package software.amazon.awssdk.dynamodb.datamodeling; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +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 java.util.Collections; +import java.util.Map; + +/** + * Object mapper for domain-object interaction with DynamoDB. + * Port of the v1 DynamoDBMapper to work with the v2 DynamoDbClient. + * Stripped to load() path for POC. + */ +public class DynamoDBMapper extends AbstractDynamoDBMapper { + + private final DynamoDbClient db; + private final DynamoDBMapperModelFactory models; + private final AttributeTransformer transformer; + + private static final Logger log = LoggerFactory.getLogger(DynamoDBMapper.class); + + public DynamoDBMapper(final DynamoDbClient dynamoDB) { + this(dynamoDB, DynamoDBMapperConfig.DEFAULT, null); + } + + public DynamoDBMapper(final DynamoDbClient dynamoDB, final DynamoDBMapperConfig config) { + this(dynamoDB, config, null); + } + + public DynamoDBMapper( + final DynamoDbClient dynamoDB, + final DynamoDBMapperConfig config, + final AttributeTransformer transformer) { + super(config); + this.db = dynamoDB; + this.transformer = transformer; + this.models = StandardModelFactories.of(); + } + + @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); + final DynamoDBMapperTableModel model = getTableModel(clazz, config); + + String tableName = getTableName(clazz, keyObject, config); + + Map key = model.convertKey(keyObject); + + GetItemRequest rq = GetItemRequest.builder() + .tableName(tableName) + .key(key) + .consistentRead(config.getConsistentReads() == DynamoDBMapperConfig.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); + final 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); + + final DynamoDBMapperTableModel model = getTableModel(clazz, parameters.getMapperConfig()); + return model.unconvert(values); + } + + private AttributeTransformer.Parameters toParameters( + final Map attributeValues, + final Class modelClass, + final String tableName, + final DynamoDBMapperConfig mapperConfig) { + + return new TransformerParameters( + getTableModel(modelClass, mapperConfig), + attributeValues, + false, + modelClass, + mapperConfig, + tableName); + } + + 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(); + } + } +} diff --git a/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBMapperConfig.java b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBMapperConfig.java new file mode 100644 index 000000000000..fd1e3ec29e08 --- /dev/null +++ b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBMapperConfig.java @@ -0,0 +1,1077 @@ +/* + * Copyright 2011-2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://aws.amazon.com/apache2.0 + * + * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and + * limitations under the License. + */ +package software.amazon.awssdk.dynamodb.datamodeling; + +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());
+ * 
+ */ +public 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) { + final TableNameOverride override = config.getTableNameOverride(); + + if (override != null) { + final String tableName = override.getTableName(); + if (tableName != null) { + return tableName; + } + } + + final StandardBeanProperties.Beans beans = StandardBeanProperties.of(clazz); + if (beans.properties().tableName() == null) { + throw new DynamoDBMappingException(clazz + " not annotated with @DynamoDBTable"); + } + + final 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.dynamodb.datamodeling.DynamoDBMapperConfig.BatchLoadRetryStrategy#getMaxRetryOnUnprocessedKeys(java.util.Map, java.util.Map) + */ + @Override + public boolean shouldRetry(final BatchLoadContext batchLoadContext) { + return false; + } + + /* (non-Javadoc) + * @see software.amazon.awssdk.dynamodb.datamodeling.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.getBatchGetItemResponse() + .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.getBatchGetItemResponse().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(builder() + .withSaveBehavior(saveBehavior) + .withConsistentReads(consistentReads) + .withTableNameOverride(tableNameOverride)); + } + + /** + * 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 in favor of the fluent {@link Builder} + * @see DynamoDBMapperConfig#builder() + */ + @Deprecated + public DynamoDBMapperConfig( + SaveBehavior saveBehavior, + ConsistentReads consistentReads, + TableNameOverride tableNameOverride, + PaginationLoadingStrategy paginationLoadingStrategy) { + + this(builder() + .withSaveBehavior(saveBehavior) + .withConsistentReads(consistentReads) + .withTableNameOverride(tableNameOverride) + .withPaginationLoadingStrategy(paginationLoadingStrategy)); + } + + @Deprecated + public DynamoDBMapperConfig(SaveBehavior saveBehavior) { + this(builder().withSaveBehavior(saveBehavior)); + } + + @Deprecated + public DynamoDBMapperConfig(ConsistentReads consistentReads) { + this(builder().withConsistentReads(consistentReads)); + } + + @Deprecated + public DynamoDBMapperConfig(TableNameOverride tableNameOverride) { + this(builder().withTableNameOverride(tableNameOverride)); + } + + @Deprecated + public DynamoDBMapperConfig(TableNameResolver tableNameResolver) { + this(builder().withTableNameResolver(tableNameResolver)); + } + + @Deprecated + public DynamoDBMapperConfig(ObjectTableNameResolver objectTableNameResolver) { + this(builder().withObjectTableNameResolver(objectTableNameResolver)); + } + + @Deprecated + public DynamoDBMapperConfig(TableNameResolver tableNameResolver, ObjectTableNameResolver objectTableNameResolver) { + this(builder().withTableNameResolver(tableNameResolver).withObjectTableNameResolver(objectTableNameResolver)); + } + + @Deprecated + public DynamoDBMapperConfig(PaginationLoadingStrategy paginationLoadingStrategy) { + this(builder().withPaginationLoadingStrategy(paginationLoadingStrategy)); + } + + @Deprecated + public DynamoDBMapperConfig(ConversionSchema conversionSchema) { + this(builder().withConversionSchema(conversionSchema)); + } + + @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/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBMapperFieldModel.java b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBMapperFieldModel.java new file mode 100644 index 000000000000..aca998487020 --- /dev/null +++ b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBMapperFieldModel.java @@ -0,0 +1,506 @@ +/* + * Copyright 2011-2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://aws.amazon.com/apache2.0 + * + * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and + * limitations under the License. + */ +package software.amazon.awssdk.dynamodb.datamodeling; + +import static software.amazon.awssdk.dynamodb.datamodeling.DynamoDBAutoGenerateStrategy.ALWAYS; +import static software.amazon.awssdk.dynamodb.datamodeling.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. + */ +public 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 software.amazon.awssdk.services.dynamodb.model.ComparisonOperator#BEGINS_WITH + * @see software.amazon.awssdk.services.dynamodb.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 software.amazon.awssdk.services.dynamodb.model.ComparisonOperator#BETWEEN + * @see software.amazon.awssdk.services.dynamodb.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 software.amazon.awssdk.services.dynamodb.model.ComparisonOperator#CONTAINS + * @see software.amazon.awssdk.services.dynamodb.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 software.amazon.awssdk.services.dynamodb.model.ComparisonOperator#EQ + * @see software.amazon.awssdk.services.dynamodb.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 software.amazon.awssdk.services.dynamodb.model.ComparisonOperator#GE + * @see software.amazon.awssdk.services.dynamodb.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 software.amazon.awssdk.services.dynamodb.model.ComparisonOperator#GT + * @see software.amazon.awssdk.services.dynamodb.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 software.amazon.awssdk.services.dynamodb.model.ComparisonOperator#IN + * @see software.amazon.awssdk.services.dynamodb.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 software.amazon.awssdk.services.dynamodb.model.ComparisonOperator#IN + * @see software.amazon.awssdk.services.dynamodb.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 software.amazon.awssdk.services.dynamodb.model.ComparisonOperator#NULL + * @see software.amazon.awssdk.services.dynamodb.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 software.amazon.awssdk.services.dynamodb.model.ComparisonOperator#LE + * @see software.amazon.awssdk.services.dynamodb.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 software.amazon.awssdk.services.dynamodb.model.ComparisonOperator#LT + * @see software.amazon.awssdk.services.dynamodb.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 software.amazon.awssdk.services.dynamodb.model.ComparisonOperator#NE + * @see software.amazon.awssdk.services.dynamodb.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 software.amazon.awssdk.services.dynamodb.model.ComparisonOperator#NOT_CONTAINS + * @see software.amazon.awssdk.services.dynamodb.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 software.amazon.awssdk.services.dynamodb.model.ComparisonOperator#NOT_NULL + * @see software.amazon.awssdk.services.dynamodb.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 software.amazon.awssdk.services.dynamodb.model.ComparisonOperator#BETWEEN + * @see software.amazon.awssdk.services.dynamodb.model.ComparisonOperator#EQ + * @see software.amazon.awssdk.services.dynamodb.model.ComparisonOperator#GE + * @see software.amazon.awssdk.services.dynamodb.model.ComparisonOperator#LE + * @see software.amazon.awssdk.services.dynamodb.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() { + final 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/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBMapperModelFactory.java b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBMapperModelFactory.java new file mode 100644 index 000000000000..e96e40a14512 --- /dev/null +++ b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBMapperModelFactory.java @@ -0,0 +1,38 @@ +/* + * Copyright 2016-2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://aws.amazon.com/apache2.0 + * + * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and + * limitations under the License. + */ +package software.amazon.awssdk.dynamodb.datamodeling; + + +/** + * {@link DynamoDBMapper} table model factory. + */ +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/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBMapperTableModel.java b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBMapperTableModel.java new file mode 100644 index 000000000000..d03ac64e50a3 --- /dev/null +++ b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBMapperTableModel.java @@ -0,0 +1,493 @@ +/* + * Copyright 2016-2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://aws.amazon.com/apache2.0 + * + * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and + * limitations under the License. + */ +package software.amazon.awssdk.dynamodb.datamodeling; + +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.services.dynamodb.model.ProjectionType.KEYS_ONLY; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.GlobalSecondaryIndex; +import software.amazon.awssdk.services.dynamodb.model.KeyType; +import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement; +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 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. + */ +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) { + final 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() { + final 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() { + final 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; + } + final Collection copies = new ArrayList(globalSecondaryIndexes.size()); + for (final 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; + } + final GlobalSecondaryIndex gsi = globalSecondaryIndexes.get(indexName); + final java.util.List keySchema = new java.util.ArrayList<>(); + for (final KeySchemaElement key : gsi.keySchema()) { + keySchema.add(KeySchemaElement.builder().attributeName(key.attributeName()).keyType(key.keyType()).build()); + } + return GlobalSecondaryIndex.builder() + .indexName(gsi.indexName()) + .projection(Projection.builder().projectionType(gsi.projection().projectionType()).build()) + .keySchema(keySchema) + .build(); + } + + /** + * 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; + } + final Collection copies = new ArrayList(localSecondaryIndexes.size()); + for (final 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; + } + final LocalSecondaryIndex lsi = localSecondaryIndexes.get(indexName); + final java.util.List keySchema = new java.util.ArrayList<>(); + for (final KeySchemaElement key : lsi.keySchema()) { + keySchema.add(KeySchemaElement.builder().attributeName(key.attributeName()).keyType(key.keyType()).build()); + } + return LocalSecondaryIndex.builder() + .indexName(lsi.indexName()) + .projection(Projection.builder().projectionType(lsi.projection().projectionType()).build()) + .keySchema(keySchema) + .build(); + } + + /** + * {@inheritDoc} + */ + @Override + public Map convert(final T object) { + final Map map = new LinkedHashMap(); + for (final DynamoDBMapperFieldModel field : fields()) { + try { + final 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) { + final T result = StandardBeanProperties.DeclaringReflect.newInstance(targetType); + if (!object.isEmpty()) { + for (final DynamoDBMapperFieldModel field : fields()) { + try { + final 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) { + final T key = StandardBeanProperties.DeclaringReflect.newInstance(targetType); + if (hashKey != null) { + final DynamoDBMapperFieldModel hk = hashKey(); + hk.set(key, hashKey); + } + if (rangeKey != null) { + final 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) { + final DynamoDBMapperFieldModel hk = this.hashKey(); + final 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) { + final Map key = new LinkedHashMap(4); + final DynamoDBMapperFieldModel hk = this.hashKey(); + final 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" + ); + } + final DynamoDBMapperFieldModel rk = this.rangeKeyIfExists(); + final 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() { + final Map map = new LinkedHashMap(); + final Map> gsiKeys = new LinkedHashMap<>(); + for (final DynamoDBMapperFieldModel field : fields.values()) { + for (final String indexName : field.globalSecondaryIndexNames(HASH)) { + if (map.containsKey(indexName)) { + throw new DynamoDBMappingException( + targetType.getSimpleName() + "[" + field.name() + "]; must not duplicate GSI " + indexName + ); + } + java.util.List ks = new java.util.ArrayList<>(); + ks.add(KeySchemaElement.builder().attributeName(field.name()).keyType(HASH).build()); + gsiKeys.put(indexName, ks); + map.put(indexName, null); // placeholder + } + } + for (final DynamoDBMapperFieldModel field : fields.values()) { + for (final String indexName : field.globalSecondaryIndexNames(RANGE)) { + if (!gsiKeys.containsKey(indexName)) { + throw new DynamoDBMappingException( + targetType.getSimpleName() + "[" + field.name() + "]; no HASH key for GSI " + indexName + ); + } + gsiKeys.get(indexName).add(KeySchemaElement.builder().attributeName(field.name()).keyType(RANGE).build()); + } + } + for (String indexName : gsiKeys.keySet()) { + map.put(indexName, GlobalSecondaryIndex.builder() + .indexName(indexName) + .projection(Projection.builder().projectionType(KEYS_ONLY).build()) + .keySchema(gsiKeys.get(indexName)) + .build()); + } + if (map.isEmpty()) { + return Collections.emptyMap(); + } + return Collections.unmodifiableMap(map); + } + + public Map localSecondaryIndexes() { + final Map map = new LinkedHashMap(); + for (final DynamoDBMapperFieldModel field : fields.values()) { + for (final String indexName : field.localSecondaryIndexNames()) { + if (map.containsKey(indexName)) { + throw new DynamoDBMappingException( + targetType.getSimpleName() + "[" + field.name() + "]; must not duplicate LSI " + indexName + ); + } + java.util.List ks = new java.util.ArrayList<>(); + ks.add(KeySchemaElement.builder().attributeName(keys.get(HASH).name()).keyType(HASH).build()); + ks.add(KeySchemaElement.builder().attributeName(field.name()).keyType(RANGE).build()); + map.put(indexName, LocalSecondaryIndex.builder() + .indexName(indexName) + .projection(Projection.builder().projectionType(KEYS_ONLY).build()) + .keySchema(ks) + .build()); + } + } + 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() { + final 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/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBMappingException.java b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBMappingException.java new file mode 100644 index 000000000000..5fef733108f4 --- /dev/null +++ b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBMappingException.java @@ -0,0 +1,41 @@ +/* + * Copyright 2011-2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://aws.amazon.com/apache2.0 + * + * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and + * limitations under the License. + */ +package software.amazon.awssdk.dynamodb.datamodeling; + +/** + * Generic exception for problems occuring when mapping DynamoDB items to Java + * objects or vice versa. Excludes service exceptions. + */ +public 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/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBNamed.java b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBNamed.java new file mode 100644 index 000000000000..8007f3354d39 --- /dev/null +++ b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBNamed.java @@ -0,0 +1,46 @@ +/* + * Copyright 2016-2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://aws.amazon.com/apache2.0 + * + * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and + * limitations under the License. + */ +package software.amazon.awssdk.dynamodb.datamodeling; + +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/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBNativeBoolean.java b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBNativeBoolean.java new file mode 100644 index 000000000000..1856c60a16ee --- /dev/null +++ b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBNativeBoolean.java @@ -0,0 +1,43 @@ +/* + * Copyright 2014-2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://aws.amazon.com/apache2.0 + * + * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and + * limitations under the License. + */ +package software.amazon.awssdk.dynamodb.datamodeling; + +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/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBRangeKey.java b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBRangeKey.java new file mode 100644 index 000000000000..a319d8a7d470 --- /dev/null +++ b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBRangeKey.java @@ -0,0 +1,42 @@ +/* + * Copyright 2011-2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://aws.amazon.com/apache2.0 + * + * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and + * limitations under the License. + */ +package software.amazon.awssdk.dynamodb.datamodeling; + +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/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBScalarAttribute.java b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBScalarAttribute.java new file mode 100644 index 000000000000..7490879c5b38 --- /dev/null +++ b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBScalarAttribute.java @@ -0,0 +1,45 @@ +/* + * Copyright 2016-2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://aws.amazon.com/apache2.0 + * + * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and + * limitations under the License. + */ +package software.amazon.awssdk.dynamodb.datamodeling; + +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 com.amazonaws.services.dynamodbv2.model.ScalarAttributeType + */ + ScalarAttributeType type(); + +} diff --git a/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBTable.java b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBTable.java new file mode 100644 index 000000000000..426ea7f71aa0 --- /dev/null +++ b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBTable.java @@ -0,0 +1,44 @@ +/* + * Copyright 2011-2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://aws.amazon.com/apache2.0 + * + * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and + * limitations under the License. + */ +package software.amazon.awssdk.dynamodb.datamodeling; + +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 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/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBTypeConverted.java b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBTypeConverted.java new file mode 100644 index 000000000000..40b7561638bc --- /dev/null +++ b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBTypeConverted.java @@ -0,0 +1,112 @@ +/* + * Copyright 2011-2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://aws.amazon.com/apache2.0 + * + * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and + * limitations under the License. + */ +package software.amazon.awssdk.dynamodb.datamodeling; + +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/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBTypeConverter.java b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBTypeConverter.java new file mode 100644 index 000000000000..608c3ed9f8da --- /dev/null +++ b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBTypeConverter.java @@ -0,0 +1,130 @@ +/* + * Copyright 2016-2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://aws.amazon.com/apache2.0 + * + * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and + * limitations under the License. + */ +package software.amazon.awssdk.dynamodb.datamodeling; + + +/** + * Interface for converting types. + * + * @param The DynamoDB standard type. + * @param The object's field/property type. + */ +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. + */ + 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/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBTypeConverterFactory.java b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBTypeConverterFactory.java new file mode 100644 index 000000000000..e59dc75f3c0a --- /dev/null +++ b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBTypeConverterFactory.java @@ -0,0 +1,172 @@ +/* + * Copyright 2016-2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://aws.amazon.com/apache2.0 + * + * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and + * limitations under the License. + */ +package software.amazon.awssdk.dynamodb.datamodeling; + +import software.amazon.awssdk.dynamodb.datamodeling.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.dynamodb.datamodeling.DynamoDBMapperConfig + */ +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 (final 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/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBTyped.java b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBTyped.java new file mode 100644 index 000000000000..9b4439b72cbe --- /dev/null +++ b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBTyped.java @@ -0,0 +1,165 @@ +/* + * Copyright 2016-2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://aws.amazon.com/apache2.0 + * + * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and + * limitations under the License. + */ +package software.amazon.awssdk.dynamodb.datamodeling; + +import software.amazon.awssdk.dynamodb.datamodeling.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 com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTypeConverted + * @see com.amazonaws.services.dynamodbv2.datamodeling.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/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBVersionAttribute.java b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBVersionAttribute.java new file mode 100644 index 000000000000..1b55f5731f9e --- /dev/null +++ b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBVersionAttribute.java @@ -0,0 +1,55 @@ +/* + * Copyright 2011-2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://aws.amazon.com/apache2.0 + * + * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and + * limitations under the License. + */ +package software.amazon.awssdk.dynamodb.datamodeling; + +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 com.amazonaws.services.dynamodbv2.datamodeling.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/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBVersioned.java b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBVersioned.java new file mode 100644 index 000000000000..7b4ae412344c --- /dev/null +++ b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/DynamoDBVersioned.java @@ -0,0 +1,158 @@ +/* + * Copyright 2011-2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://aws.amazon.com/apache2.0 + * + * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and + * limitations under the License. + */ +package software.amazon.awssdk.dynamodb.datamodeling; + + +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; + +import static software.amazon.awssdk.dynamodb.datamodeling.StandardTypeConverters.Scalar; + +/** + * 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 com.amazonaws.services.dynamodbv2.datamodeling.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 (final 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/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/IDynamoDBMapper.java b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/IDynamoDBMapper.java new file mode 100644 index 000000000000..37054fcc89d8 --- /dev/null +++ b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/IDynamoDBMapper.java @@ -0,0 +1,42 @@ +/* + * Copyright 2015-2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://aws.amazon.com/apache2.0 + * + * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and + * limitations under the License. + */ +package software.amazon.awssdk.dynamodb.datamodeling; + +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import java.util.Map; + +/** + * Interface for DynamoDBMapper operations. Stripped to load path for POC. + */ +public interface IDynamoDBMapper { + + DynamoDBMapperTableModel getTableModel(Class clazz); + + DynamoDBMapperTableModel getTableModel(Class clazz, DynamoDBMapperConfig config); + + T load(Class clazz, Object hashKey, DynamoDBMapperConfig config); + + T load(Class clazz, Object hashKey); + + T load(Class clazz, Object hashKey, Object rangeKey); + + T load(Class clazz, Object hashKey, Object rangeKey, DynamoDBMapperConfig config); + + T load(T keyObject); + + T load(T keyObject, DynamoDBMapperConfig config); + + T marshallIntoObject(Class clazz, Map itemAttributes); +} diff --git a/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/ItemConverter.java b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/ItemConverter.java new file mode 100644 index 000000000000..e19bbfac57f3 --- /dev/null +++ b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/ItemConverter.java @@ -0,0 +1,78 @@ +/* + * Copyright 2014-2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://aws.amazon.com/apache2.0 + * + * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and + * limitations under the License. + */ +package software.amazon.awssdk.dynamodb.datamodeling; + +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}. + */ +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/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/StandardAnnotationMaps.java b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/StandardAnnotationMaps.java new file mode 100644 index 000000000000..4c0334b95471 --- /dev/null +++ b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/StandardAnnotationMaps.java @@ -0,0 +1,445 @@ +/* + * Copyright 2016-2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://aws.amazon.com/apache2.0 + * + * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and + * limitations under the License. + */ +package software.amazon.awssdk.dynamodb.datamodeling; + +import static software.amazon.awssdk.dynamodb.datamodeling.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.dynamodb.datamodeling.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. + */ +final class StandardAnnotationMaps { + + /** + * Gets all the DynamoDB annotations for a given class. + */ + static final TableMap of(Class clazz) { + final 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) { + final Class targetType = (Class)getter.getReturnType(); + final 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; + } + + final 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) { + final 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() { + final 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) { + final 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) { + final 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() { + final Map attributes = new LinkedHashMap(); + for (final 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() { + final 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() { + final 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() { + final DynamoDBHashKey hashKey = actualOf(DynamoDBHashKey.class); + if (hashKey != null && !hashKey.attributeName().isEmpty()) { + return hashKey.attributeName(); + } + final DynamoDBIndexHashKey indexHashKey = actualOf(DynamoDBIndexHashKey.class); + if (indexHashKey != null && !indexHashKey.attributeName().isEmpty()) { + return indexHashKey.attributeName(); + } + final DynamoDBRangeKey rangeKey = actualOf(DynamoDBRangeKey.class); + if (rangeKey != null && !rangeKey.attributeName().isEmpty()) { + return rangeKey.attributeName(); + } + final DynamoDBIndexRangeKey indexRangeKey = actualOf(DynamoDBIndexRangeKey.class); + if (indexRangeKey != null && !indexRangeKey.attributeName().isEmpty()) { + return indexRangeKey.attributeName(); + } + final DynamoDBAttribute attribute = actualOf(DynamoDBAttribute.class); + if (attribute != null && !attribute.attributeName().isEmpty()) { + return attribute.attributeName(); + } + final DynamoDBVersionAttribute versionAttribute = actualOf(DynamoDBVersionAttribute.class); + if (versionAttribute != null && !versionAttribute.attributeName().isEmpty()) { + return versionAttribute.attributeName(); + } + final DynamoDBScalarAttribute scalarAttribute = actualOf(DynamoDBScalarAttribute.class); + if (scalarAttribute != null && !scalarAttribute.attributeName().isEmpty()) { + return scalarAttribute.attributeName(); + } + final DynamoDBNamed annotation = actualOf(DynamoDBNamed.class); + if (annotation != null && !annotation.value().isEmpty()) { + return annotation.value(); + } + return this.defaultName; + } + + /** + * {@inheritDoc} + */ + @Override + public KeyType keyType() { + final 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() { + final Map> gsis = new EnumMap>(KeyType.class); + final 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"); + } + } + final 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() { + final 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 (final Annotation a1 : annotations) { + putIfAnnotated(a1.annotationType(), a1); + for (final Annotation a2 : a1.annotationType().getAnnotations()) { + if (putIfAnnotated(a2.annotationType(), a1)) { + for (final 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/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/StandardBeanProperties.java b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/StandardBeanProperties.java new file mode 100644 index 000000000000..968c31fb4915 --- /dev/null +++ b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/StandardBeanProperties.java @@ -0,0 +1,266 @@ +/* + * Copyright 2016-2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://aws.amazon.com/apache2.0 + * + * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and + * limitations under the License. + */ +package software.amazon.awssdk.dynamodb.datamodeling; + +import software.amazon.awssdk.dynamodb.datamodeling.DynamoDBMapperFieldModel.DynamoDBAttributeType; +import software.amazon.awssdk.dynamodb.datamodeling.DynamoDBMapperFieldModel.Reflect; +import software.amazon.awssdk.dynamodb.datamodeling.StandardAnnotationMaps.FieldMap; +import software.amazon.awssdk.dynamodb.datamodeling.StandardAnnotationMaps.TableMap; + +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} + */ +final class 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)) { + final TableMap annotations = StandardAnnotationMaps.of(clazz); + final 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 { + final 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) { + final 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 (final Method method : clazz.getMethods()) { + if (canMap(method, inherited)) { + final FieldMap annotations = StandardAnnotationMaps.of(method, null); + if (!annotations.ignored()) { + final 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 { + final 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 (final Method method : targetType.getMethods()) { + if (canMap(method, true)) { + String name = fieldNameOf(method); + if ((name = attributes.remove(name)) == null) { + continue; + } + final FieldMap annotations = StandardAnnotationMaps.of(method, name); + if (!annotations.ignored()) { + final 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) { + final String name = getter.getName().replaceFirst("^(get|is)",""); + return name.substring(0, 1).toLowerCase(java.util.Locale.ROOT) + name.substring(1); + } + +} diff --git a/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/StandardModelFactories.java b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/StandardModelFactories.java new file mode 100644 index 000000000000..7a67d7803b30 --- /dev/null +++ b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/StandardModelFactories.java @@ -0,0 +1,711 @@ +/* + * Copyright 2016-2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://aws.amazon.com/apache2.0 + * + * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and + * limitations under the License. + */ +package software.amazon.awssdk.dynamodb.datamodeling; + +import software.amazon.awssdk.dynamodb.datamodeling.DynamoDBMapperFieldModel.DynamoDBAttributeType; +import software.amazon.awssdk.dynamodb.datamodeling.DynamoDBMapperFieldModel.Reflect; +import software.amazon.awssdk.dynamodb.datamodeling.DynamoDBMapperModelFactory.TableFactory; +import software.amazon.awssdk.dynamodb.datamodeling.DynamoDBTypeConverter.AbstractConverter; +import software.amazon.awssdk.dynamodb.datamodeling.DynamoDBTypeConverter.DelegateConverter; +import software.amazon.awssdk.dynamodb.datamodeling.StandardBeanProperties.Bean; +import software.amazon.awssdk.dynamodb.datamodeling.StandardBeanProperties.Beans; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.joda.time.DateTime; + +import java.nio.ByteBuffer; +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.dynamodb.datamodeling.StandardTypeConverters.Scalar.BOOLEAN; +import static software.amazon.awssdk.dynamodb.datamodeling.StandardTypeConverters.Scalar.DEFAULT; +import static software.amazon.awssdk.dynamodb.datamodeling.StandardTypeConverters.Scalar.STRING; +import static software.amazon.awssdk.dynamodb.datamodeling.StandardTypeConverters.Vector.LIST; +import static software.amazon.awssdk.dynamodb.datamodeling.StandardTypeConverters.Vector.MAP; +import static software.amazon.awssdk.dynamodb.datamodeling.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. + */ +final class StandardModelFactories { + + private static final Logger LOG = LoggerFactory.getLogger(StandardModelFactories.class); + + /** + * Creates the standard {@link DynamoDBMapperModelFactory} factory. + */ + static final DynamoDBMapperModelFactory of() { + return new StandardModelFactory(); + } + + /** + * {@link TableFactory} mapped by {@link ConversionSchema}. + */ + private static final class StandardModelFactory implements DynamoDBMapperModelFactory { + private final ConcurrentMap cache; + + private StandardModelFactory() { + this.cache = new ConcurrentHashMap(); + } + + @Override + public TableFactory getTableFactory(DynamoDBMapperConfig config) { + final ConversionSchema schema = config.getConversionSchema(); + if (!cache.containsKey(schema)) { + RuleFactory rules = rulesOf(config, this); + rules = new ConversionSchemas.ItemConverterRuleFactory(config, 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 (final 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, DynamoDBMapperModelFactory models) { + final boolean ver1 = (config.getConversionSchema() == ConversionSchemas.V1); + final boolean ver2 = (config.getConversionSchema() == ConversionSchemas.V2); + final boolean v2Compatible = (config.getConversionSchema() == ConversionSchemas.V2_COMPATIBLE); + + final DynamoDBTypeConverterFactory.Builder scalars = config.getTypeConverterFactory().override(); + + final 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 (final 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()); + } + public AttributeValue get(AttributeValue o) { + return o; + } + 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()); + } + public String get(AttributeValue value) { + return value.s(); + } + 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()); + } + public String get(AttributeValue value) { + return value.n(); + } + 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()); + } + public Long get(AttributeValue value) { + return Long.valueOf(value.n()); + } + 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()); + } + public ByteBuffer get(AttributeValue value) { + return value.b() == null ? null : value.b().asByteBuffer(); + } + public AttributeValue build(ByteBuffer o) { + return AttributeValue.builder().b(software.amazon.awssdk.core.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()); + } + public List get(AttributeValue value) { + return value.ss(); + } + 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()); + } + public List get(AttributeValue value) { + return value.ns(); + } + 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()); + } + public List get(AttributeValue value) { + if (!value.hasBs()) return null; + java.util.List result = new java.util.ArrayList<>(value.bs().size()); + for (software.amazon.awssdk.core.SdkBytes sb : value.bs()) { result.add(sb.asByteBuffer()); } + return result; + } + public AttributeValue build(List o) { + java.util.List sdkBytes = new java.util.ArrayList<>(o.size()); + for (ByteBuffer bb : o) { sdkBytes.add(software.amazon.awssdk.core.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()); + } + public Boolean get(AttributeValue o) { + return o.bool(); + } + 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. + */ + 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}. + */ + 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()); + } + public List get(AttributeValue value) { + return value.l(); + } + 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.l() == null && o.ns() != null) { + 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()); + } + public List get(AttributeValue value) { + return value.l(); + } + 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() + ); + } + public Map get(AttributeValue value) { + return value.m(); + } + 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()); + } + public Map get(AttributeValue value) { + return value.m(); + } + 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; + } + public T get(AttributeValue value) { + throw new DynamoDBMappingException("not supported; requires @DynamoDBTyped or @DynamoDBTypeConverted"); + } + 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 AttributeValue from the given value. + */ + public abstract AttributeValue build(S o); + /** + * Extracts the scalar value from an AttributeValue. + */ + public abstract S get(AttributeValue o); + @Override + public S unconvert(final AttributeValue o) { + final 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/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/StandardTypeConverters.java b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/StandardTypeConverters.java new file mode 100644 index 000000000000..a85946003fa2 --- /dev/null +++ b/test/dynamodb-mapper-v2/src/main/java/software/amazon/awssdk/dynamodb/datamodeling/StandardTypeConverters.java @@ -0,0 +1,990 @@ +/* + * Copyright 2016-2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://aws.amazon.com/apache2.0 + * + * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and + * limitations under the License. + */ +package software.amazon.awssdk.dynamodb.datamodeling; + +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.dynamodb.datamodeling.DynamoDBTypeConverter + */ +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) { + final Scalar source = Scalar.of(sourceType), target = Scalar.of(targetType); + final Converter toSource = source.getConverter(sourceType, target.type()); + final 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 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 (final 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) { + final List vector = new ArrayList(o.size()); + for (final T t : o) { + vector.add(scalar.convert(t)); + } + return vector; + } + + List unconvert(Collection o, DynamoDBTypeConverter scalar) { + final List vector = new ArrayList(o.size()); + for (final 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) { + final Map vector = new LinkedHashMap(); + for (final Map.Entry t : o.entrySet()) { + vector.put(t.getKey(), scalar.convert(t.getValue())); + } + return vector; + } + + Map unconvert(Map o, DynamoDBTypeConverter scalar) { + final Map vector = new LinkedHashMap(); + for (final 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) { + final Set vector = new LinkedHashSet(); + for (final 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 (final 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(); + } + final 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) { + final 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) { + final 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 Date.from(java.time.Instant.parse(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) { + final 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 java.time.format.DateTimeFormatter.ISO_INSTANT + .format(o.toInstant()); + } + }; + + 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) { + final 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) { + final 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/test/sdk-benchmarks/pom.xml b/test/sdk-benchmarks/pom.xml index c1bf7c74e268..8bedbec027eb 100644 --- a/test/sdk-benchmarks/pom.xml +++ b/test/sdk-benchmarks/pom.xml @@ -76,6 +76,12 @@ ${sdk-v1.version} + + software.amazon.awssdk + dynamodb-mapper-v2 + 1.0.0-SNAPSHOT + + software.amazon.awssdk dynamodb diff --git a/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/enhanced/dynamodb/EnhancedClientGetV1MapperComparisonBenchmark.java b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/enhanced/dynamodb/EnhancedClientGetV1MapperComparisonBenchmark.java index 12bc62d18454..7a5748818b48 100644 --- a/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/enhanced/dynamodb/EnhancedClientGetV1MapperComparisonBenchmark.java +++ b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/enhanced/dynamodb/EnhancedClientGetV1MapperComparisonBenchmark.java @@ -46,7 +46,7 @@ public class EnhancedClientGetV1MapperComparisonBenchmark { private static final V1ItemFactory V1_ITEM_FACTORY = new V1ItemFactory(); @Benchmark - public Object v2Get(TestState s) { + public Object v2EnhancedGet(TestState s) { return s.v2Table.getItem(s.key); } @@ -55,6 +55,11 @@ public Object v1Get(TestState s) { return s.v1DdbMapper.load(s.testItem.v1Key); } + @Benchmark + public Object v2MapperGet(TestState s) { + return s.v2DdbMapper.load(s.testItem.v2MapperKey); + } + private static DynamoDbClient getV2Client(Blackhole bh, GetItemResponse getItemResponse) { return new V2TestDynamoDbGetItemClient(bh, getItemResponse); } @@ -72,17 +77,22 @@ public static class TestState { private DynamoDbTable v2Table; private DynamoDBMapper v1DdbMapper; - + private software.amazon.awssdk.dynamodb.datamodeling.DynamoDBMapper v2DdbMapper; @Setup public void setup(Blackhole bh) { + // Enhanced Client DynamoDbEnhancedClient v2DdbEnh = DynamoDbEnhancedClient.builder() .dynamoDbClient(getV2Client(bh, testItem.v2Response)) .build(); - v2Table = v2DdbEnh.table(testItem.name(), testItem.schema); + // V1 Mapper v1DdbMapper = new DynamoDBMapper(getV1Client(bh, testItem.v1Response)); + + // V2 Mapper (new) — uses v2 client, returns v2 response + v2DdbMapper = new software.amazon.awssdk.dynamodb.datamodeling.DynamoDBMapper( + getV2Client(bh, testItem.v2Response)); } public enum TestItem { @@ -91,7 +101,9 @@ public enum TestItem { GetItemResponse.builder().item(V2_ITEM_FACTORY.tiny()).build(), new V1ItemFactory.V1TinyBean("hashKey"), - new GetItemResult().withItem(V1_ITEM_FACTORY.tiny()) + new GetItemResult().withItem(V1_ITEM_FACTORY.tiny()), + + new V2MapperItemFactory.V2MapperTinyBean("hashKey") ), SMALL( @@ -99,7 +111,9 @@ public enum TestItem { GetItemResponse.builder().item(V2_ITEM_FACTORY.small()).build(), new V1ItemFactory.V1SmallBean("hashKey"), - new GetItemResult().withItem(V1_ITEM_FACTORY.small()) + new GetItemResult().withItem(V1_ITEM_FACTORY.small()), + + new V2MapperItemFactory.V2MapperSmallBean("hashKey") ), HUGE( @@ -107,7 +121,9 @@ public enum TestItem { GetItemResponse.builder().item(V2_ITEM_FACTORY.huge()).build(), new V1ItemFactory.V1HugeBean("hashKey"), - new GetItemResult().withItem(V1_ITEM_FACTORY.huge()) + new GetItemResult().withItem(V1_ITEM_FACTORY.huge()), + + new V2MapperItemFactory.V2MapperHugeBean("hashKey") ), HUGE_FLAT( @@ -115,28 +131,33 @@ public enum TestItem { GetItemResponse.builder().item(V2_ITEM_FACTORY.hugeFlat()).build(), new V1ItemFactory.V1HugeBeanFlat("hashKey"), - new GetItemResult().withItem(V1_ITEM_FACTORY.hugeFlat()) + new GetItemResult().withItem(V1_ITEM_FACTORY.hugeFlat()), + + new V2MapperItemFactory.V2MapperHugeBeanFlat("hashKey") ), ; - // V2 + // V2 Enhanced Client private TableSchema schema; private GetItemResponse v2Response; - // V1 + // V1 Mapper private Object v1Key; private GetItemResult v1Response; - TestItem(TableSchema schema, - GetItemResponse v2Response, + // V2 Mapper (new) + private Object v2MapperKey; - Object v1Key, - GetItemResult v1Response) { + TestItem(TableSchema schema, + GetItemResponse v2Response, + Object v1Key, + GetItemResult v1Response, + Object v2MapperKey) { this.schema = schema; this.v2Response = v2Response; - this.v1Key = v1Key; this.v1Response = v1Response; + this.v2MapperKey = v2MapperKey; } } } diff --git a/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/enhanced/dynamodb/V2MapperItemFactory.java b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/enhanced/dynamodb/V2MapperItemFactory.java new file mode 100644 index 000000000000..04f9effa93b9 --- /dev/null +++ b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/enhanced/dynamodb/V2MapperItemFactory.java @@ -0,0 +1,596 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.benchmark.enhanced.dynamodb; + +import java.lang.reflect.Method; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.dynamodb.datamodeling.DynamoDBAttribute; +import software.amazon.awssdk.dynamodb.datamodeling.DynamoDBHashKey; +import software.amazon.awssdk.dynamodb.datamodeling.DynamoDBTable; + +/** + * Bean classes for the v2 DynamoDBMapper (dynamodb-mapper-v2). + * Mirrors V1ItemFactory beans but uses software.amazon.awssdk.dynamodb.datamodeling annotations. + */ +public final class V2MapperItemFactory { + + @DynamoDBTable(tableName = "V2MapperTinyBean") + public static class V2MapperTinyBean extends ItemFactory.TinyBean { + + public V2MapperTinyBean() { + } + + public V2MapperTinyBean(String stringAttr) { + super.setStringAttr(stringAttr); + } + + @DynamoDBHashKey + @Override + public String getStringAttr() { + return super.getStringAttr(); + } + } + + @DynamoDBTable(tableName = "V2MapperSmallBean") + public static class V2MapperSmallBean extends ItemFactory.SmallBean { + private ByteBuffer binaryAttr; + + public V2MapperSmallBean() { + } + + public V2MapperSmallBean(String stringAttr) { + super.setStringAttr(stringAttr); + } + + @DynamoDBHashKey + @Override + public String getStringAttr() { + return super.getStringAttr(); + } + + @DynamoDBAttribute(attributeName = "binaryAttr") + public ByteBuffer getBinaryAttrV2() { + return binaryAttr; + } + + public void setBinaryAttrV2(ByteBuffer binaryAttr) { + this.binaryAttr = binaryAttr; + } + + @DynamoDBAttribute + @Override + public List getListAttr() { + return super.getListAttr(); + } + + static V2MapperSmallBean fromSmallBean(ItemFactory.SmallBean sb) { + V2MapperSmallBean b = new V2MapperSmallBean(); + b.setStringAttr(sb.getStringAttr()); + b.setBinaryAttrV2(sb.getBinaryAttr().asByteBuffer()); + b.setListAttr(sb.getListAttr()); + return b; + } + } + + @DynamoDBTable(tableName = "V2MapperHugeBean") + public static class V2MapperHugeBean extends ItemFactory.HugeBean { + private ByteBuffer binaryAttr; + private Map mapAttr1; + private Map> mapAttr2; + private Map>>> mapAttr3; + + public V2MapperHugeBean() { + } + + public V2MapperHugeBean(String stringAttr) { + super.setStringAttr(stringAttr); + } + + @DynamoDBHashKey + @Override + public String getStringAttr() { + return super.getStringAttr(); + } + + @DynamoDBAttribute + @Override + public String getHashKey() { + return super.getHashKey(); + } + + @DynamoDBAttribute(attributeName = "binaryAttr") + public ByteBuffer getBinaryAttrV2() { + return binaryAttr; + } + + public void setBinaryAttrV2(ByteBuffer binaryAttr) { + this.binaryAttr = binaryAttr; + } + + @DynamoDBAttribute + @Override + public List getListAttr() { + return super.getListAttr(); + } + + @DynamoDBAttribute(attributeName = "mapAttr1") + public Map getMapAttr1V2() { + return mapAttr1; + } + + public void setMapAttr1V2(Map mapAttr1) { + this.mapAttr1 = mapAttr1; + } + + @DynamoDBAttribute(attributeName = "mapAttr2") + public Map> getMapAttr2V2() { + return mapAttr2; + } + + public void setMapAttr2V2(Map> mapAttr2) { + this.mapAttr2 = mapAttr2; + } + + @DynamoDBAttribute(attributeName = "mapAttr3") + public Map>>> getMapAttr3V2() { + return mapAttr3; + } + + public void setMapAttr3V2( + Map>>> mapAttr3) { + this.mapAttr3 = mapAttr3; + } + + static V2MapperHugeBean fromHugeBean(ItemFactory.HugeBean hb) { + V2MapperHugeBean b = new V2MapperHugeBean(); + b.setHashKey(hb.getHashKey()); + b.setStringAttr(hb.getStringAttr()); + b.setBinaryAttrV2(hb.getBinaryAttr().asByteBuffer()); + b.setListAttr(hb.getListAttr()); + b.setMapAttr1V2(hb.getMapAttr1().entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, + e -> e.getValue().asByteBuffer()))); + b.setMapAttr2V2(hb.getMapAttr2().entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, + e -> e.getValue().stream() + .map(SdkBytes::asByteBuffer) + .collect(Collectors.toList())))); + b.setMapAttr3V2(hb.getMapAttr3().entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, + e -> e.getValue().stream() + .map(m -> m.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + ee -> ee.getValue().stream() + .map(SdkBytes::asByteBuffer) + .collect(Collectors.toList())))) + .collect(Collectors.toList())))); + return b; + } + } + + @DynamoDBTable(tableName = "V2MapperHugeBeanFlat") + public static class V2MapperHugeBeanFlat extends ItemFactory.HugeBeanFlat { + + public V2MapperHugeBeanFlat() { + } + + public V2MapperHugeBeanFlat(String stringAttr) { + this.setStringAttr1(stringAttr); + } + + @DynamoDBAttribute(attributeName = "stringAttr1") + @DynamoDBHashKey + @Override + public String getStringAttr1() { + return super.getStringAttr1(); + } + + @DynamoDBAttribute(attributeName = "stringAttr2") + @Override + public String getStringAttr2() { + return super.getStringAttr2(); + } + + @DynamoDBAttribute(attributeName = "stringAttr3") + @Override + public String getStringAttr3() { + return super.getStringAttr3(); + } + + @DynamoDBAttribute(attributeName = "stringAttr4") + @Override + public String getStringAttr4() { + return super.getStringAttr4(); + } + + @DynamoDBAttribute(attributeName = "stringAttr5") + @Override + public String getStringAttr5() { + return super.getStringAttr5(); + } + + @DynamoDBAttribute(attributeName = "stringAttr6") + @Override + public String getStringAttr6() { + return super.getStringAttr6(); + } + + @DynamoDBAttribute(attributeName = "stringAttr7") + @Override + public String getStringAttr7() { + return super.getStringAttr7(); + } + + @DynamoDBAttribute(attributeName = "stringAttr8") + @Override + public String getStringAttr8() { + return super.getStringAttr8(); + } + + @DynamoDBAttribute(attributeName = "stringAttr9") + @Override + public String getStringAttr9() { + return super.getStringAttr9(); + } + + @DynamoDBAttribute(attributeName = "stringAttr10") + @Override + public String getStringAttr10() { + return super.getStringAttr10(); + } + + @DynamoDBAttribute(attributeName = "stringAttr11") + @Override + public String getStringAttr11() { + return super.getStringAttr11(); + } + + @DynamoDBAttribute(attributeName = "stringAttr12") + @Override + public String getStringAttr12() { + return super.getStringAttr12(); + } + + @DynamoDBAttribute(attributeName = "stringAttr13") + @Override + public String getStringAttr13() { + return super.getStringAttr13(); + } + + @DynamoDBAttribute(attributeName = "stringAttr14") + @Override + public String getStringAttr14() { + return super.getStringAttr14(); + } + + @DynamoDBAttribute(attributeName = "stringAttr15") + @Override + public String getStringAttr15() { + return super.getStringAttr15(); + } + + @DynamoDBAttribute(attributeName = "stringAttr16") + @Override + public String getStringAttr16() { + return super.getStringAttr16(); + } + + @DynamoDBAttribute(attributeName = "stringAttr17") + @Override + public String getStringAttr17() { + return super.getStringAttr17(); + } + + @DynamoDBAttribute(attributeName = "stringAttr18") + @Override + public String getStringAttr18() { + return super.getStringAttr18(); + } + + @DynamoDBAttribute(attributeName = "stringAttr19") + @Override + public String getStringAttr19() { + return super.getStringAttr19(); + } + + @DynamoDBAttribute(attributeName = "stringAttr20") + @Override + public String getStringAttr20() { + return super.getStringAttr20(); + } + + @DynamoDBAttribute(attributeName = "stringAttr21") + @Override + public String getStringAttr21() { + return super.getStringAttr21(); + } + + @DynamoDBAttribute(attributeName = "stringAttr22") + @Override + public String getStringAttr22() { + return super.getStringAttr22(); + } + + @DynamoDBAttribute(attributeName = "stringAttr23") + @Override + public String getStringAttr23() { + return super.getStringAttr23(); + } + + @DynamoDBAttribute(attributeName = "stringAttr24") + @Override + public String getStringAttr24() { + return super.getStringAttr24(); + } + + @DynamoDBAttribute(attributeName = "stringAttr25") + @Override + public String getStringAttr25() { + return super.getStringAttr25(); + } + + @DynamoDBAttribute(attributeName = "stringAttr26") + @Override + public String getStringAttr26() { + return super.getStringAttr26(); + } + + @DynamoDBAttribute(attributeName = "stringAttr27") + @Override + public String getStringAttr27() { + return super.getStringAttr27(); + } + + @DynamoDBAttribute(attributeName = "stringAttr28") + @Override + public String getStringAttr28() { + return super.getStringAttr28(); + } + + @DynamoDBAttribute(attributeName = "stringAttr29") + @Override + public String getStringAttr29() { + return super.getStringAttr29(); + } + + @DynamoDBAttribute(attributeName = "stringAttr30") + @Override + public String getStringAttr30() { + return super.getStringAttr30(); + } + + @DynamoDBAttribute(attributeName = "stringAttr31") + @Override + public String getStringAttr31() { + return super.getStringAttr31(); + } + + @DynamoDBAttribute(attributeName = "stringAttr32") + @Override + public String getStringAttr32() { + return super.getStringAttr32(); + } + + @DynamoDBAttribute(attributeName = "stringAttr33") + @Override + public String getStringAttr33() { + return super.getStringAttr33(); + } + + @DynamoDBAttribute(attributeName = "stringAttr34") + @Override + public String getStringAttr34() { + return super.getStringAttr34(); + } + + @DynamoDBAttribute(attributeName = "stringAttr35") + @Override + public String getStringAttr35() { + return super.getStringAttr35(); + } + + @DynamoDBAttribute(attributeName = "stringAttr36") + @Override + public String getStringAttr36() { + return super.getStringAttr36(); + } + + @DynamoDBAttribute(attributeName = "stringAttr37") + @Override + public String getStringAttr37() { + return super.getStringAttr37(); + } + + @DynamoDBAttribute(attributeName = "stringAttr38") + @Override + public String getStringAttr38() { + return super.getStringAttr38(); + } + + @DynamoDBAttribute(attributeName = "stringAttr39") + @Override + public String getStringAttr39() { + return super.getStringAttr39(); + } + + @DynamoDBAttribute(attributeName = "stringAttr40") + @Override + public String getStringAttr40() { + return super.getStringAttr40(); + } + + @DynamoDBAttribute(attributeName = "stringAttr41") + @Override + public String getStringAttr41() { + return super.getStringAttr41(); + } + + @DynamoDBAttribute(attributeName = "stringAttr42") + @Override + public String getStringAttr42() { + return super.getStringAttr42(); + } + + @DynamoDBAttribute(attributeName = "stringAttr43") + @Override + public String getStringAttr43() { + return super.getStringAttr43(); + } + + @DynamoDBAttribute(attributeName = "stringAttr44") + @Override + public String getStringAttr44() { + return super.getStringAttr44(); + } + + @DynamoDBAttribute(attributeName = "stringAttr45") + @Override + public String getStringAttr45() { + return super.getStringAttr45(); + } + + @DynamoDBAttribute(attributeName = "stringAttr46") + @Override + public String getStringAttr46() { + return super.getStringAttr46(); + } + + @DynamoDBAttribute(attributeName = "stringAttr47") + @Override + public String getStringAttr47() { + return super.getStringAttr47(); + } + + @DynamoDBAttribute(attributeName = "stringAttr48") + @Override + public String getStringAttr48() { + return super.getStringAttr48(); + } + + @DynamoDBAttribute(attributeName = "stringAttr49") + @Override + public String getStringAttr49() { + return super.getStringAttr49(); + } + + @DynamoDBAttribute(attributeName = "stringAttr50") + @Override + public String getStringAttr50() { + return super.getStringAttr50(); + } + + @DynamoDBAttribute(attributeName = "stringAttr51") + @Override + public String getStringAttr51() { + return super.getStringAttr51(); + } + + @DynamoDBAttribute(attributeName = "stringAttr52") + @Override + public String getStringAttr52() { + return super.getStringAttr52(); + } + + @DynamoDBAttribute(attributeName = "stringAttr53") + @Override + public String getStringAttr53() { + return super.getStringAttr53(); + } + + @DynamoDBAttribute(attributeName = "stringAttr54") + @Override + public String getStringAttr54() { + return super.getStringAttr54(); + } + + @DynamoDBAttribute(attributeName = "stringAttr55") + @Override + public String getStringAttr55() { + return super.getStringAttr55(); + } + + @DynamoDBAttribute(attributeName = "stringAttr56") + @Override + public String getStringAttr56() { + return super.getStringAttr56(); + } + + @DynamoDBAttribute(attributeName = "stringAttr57") + @Override + public String getStringAttr57() { + return super.getStringAttr57(); + } + + @DynamoDBAttribute(attributeName = "stringAttr58") + @Override + public String getStringAttr58() { + return super.getStringAttr58(); + } + + @DynamoDBAttribute(attributeName = "stringAttr59") + @Override + public String getStringAttr59() { + return super.getStringAttr59(); + } + + @DynamoDBAttribute(attributeName = "stringAttr60") + @Override + public String getStringAttr60() { + return super.getStringAttr60(); + } + + @DynamoDBAttribute(attributeName = "stringAttr61") + @Override + public String getStringAttr61() { + return super.getStringAttr61(); + } + + @DynamoDBAttribute(attributeName = "stringAttr62") + @Override + public String getStringAttr62() { + return super.getStringAttr62(); + } + + @DynamoDBAttribute(attributeName = "stringAttr63") + @Override + public String getStringAttr63() { + return super.getStringAttr63(); + } + + public static V2MapperHugeBeanFlat fromHugeBeanFlat( + ItemFactory.HugeBeanFlat b) { + V2MapperHugeBeanFlat bean = new V2MapperHugeBeanFlat(); + for (int i = 1; i <= 63; ++i) { + try { + Method setter = V2MapperHugeBeanFlat.class + .getMethod("setStringAttr" + i, String.class); + Method getter = ItemFactory.HugeBeanFlat.class + .getMethod("getStringAttr" + i); + setter.invoke(bean, getter.invoke(b)); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + return bean; + } + } +}