/* * Copyright 2016-2023 Amazon.com, Inc. or its affiliates. All 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 com.amazonaws.services.dynamodbv2.datamodeling; import static com.amazonaws.services.dynamodbv2.model.KeyType.HASH; import static com.amazonaws.services.dynamodbv2.model.KeyType.RANGE; import static com.amazonaws.services.dynamodbv2.model.ProjectionType.KEYS_ONLY; import com.amazonaws.services.dynamodbv2.model.AttributeValue; import com.amazonaws.services.dynamodbv2.model.GlobalSecondaryIndex; import com.amazonaws.services.dynamodbv2.model.KeyType; import com.amazonaws.services.dynamodbv2.model.KeySchemaElement; import com.amazonaws.services.dynamodbv2.model.LocalSecondaryIndex; import com.amazonaws.services.dynamodbv2.model.Projection; import com.amazonaws.services.dynamodbv2.model.ProjectionType; import java.util.Arrays; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.EnumMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Map; /** * Table model. * * @param The object type. */ 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 GlobalSecondaryIndex copy = new GlobalSecondaryIndex().withIndexName(gsi.getIndexName()); copy.withProjection(new Projection().withProjectionType(gsi.getProjection().getProjectionType())); for (final KeySchemaElement key : gsi.getKeySchema()) { copy.withKeySchema(new KeySchemaElement(key.getAttributeName(), key.getKeyType())); } return copy; } /** * Gets the local secondary indexes for the given class. * @param indexNames The index names. * @return The map of index name to LocalSecondaryIndexes. */ public Collection localSecondaryIndexes() { if (localSecondaryIndexes.isEmpty()) { return null; } 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 LocalSecondaryIndex copy = new LocalSecondaryIndex().withIndexName(lsi.getIndexName()); copy.withProjection(new Projection().withProjectionType(lsi.getProjection().getProjectionType())); for (final KeySchemaElement key : lsi.getKeySchema()) { copy.withKeySchema(new KeySchemaElement(key.getAttributeName(), key.getKeyType())); } return copy; } /** * {@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(); for (final DynamoDBMapperFieldModel field : fields.values()) { for (final String indexName : field.globalSecondaryIndexNames(HASH)) { final GlobalSecondaryIndex gsi = new GlobalSecondaryIndex().withIndexName(indexName); if (map.put(indexName, gsi) != null) { throw new DynamoDBMappingException( targetType.getSimpleName() + "[" + field.name() + "]; must not duplicate GSI " + indexName ); } gsi.withProjection(new Projection().withProjectionType(KEYS_ONLY)); gsi.withKeySchema(new KeySchemaElement(field.name(), HASH)); } } for (final DynamoDBMapperFieldModel field : fields.values()) { for (final String indexName : field.globalSecondaryIndexNames(RANGE)) { final GlobalSecondaryIndex gsi = map.get(indexName); if (gsi == null) { throw new DynamoDBMappingException( targetType.getSimpleName() + "[" + field.name() + "]; no HASH key for GSI " + indexName ); } gsi.withKeySchema(new KeySchemaElement(field.name(), RANGE)); } } 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()) { final LocalSecondaryIndex lsi = new LocalSecondaryIndex().withIndexName(indexName); if (map.put(indexName, lsi) != null) { throw new DynamoDBMappingException( targetType.getSimpleName() + "[" + field.name() + "]; must not duplicate LSI " + indexName ); } lsi.withProjection(new Projection().withProjectionType(KEYS_ONLY)); lsi.withKeySchema(new KeySchemaElement(keys.get(HASH).name(), HASH)); lsi.withKeySchema(new KeySchemaElement(field.name(), RANGE)); } } if (map.isEmpty()) { return Collections.emptyMap(); } return Collections.unmodifiableMap(map); } private Map> versions() { if (versions.isEmpty()) { return Collections.>emptyMap(); } return Collections.unmodifiableMap(versions); } public Map> fields() { if (fields.isEmpty()) { return Collections.>emptyMap(); } return Collections.unmodifiableMap(fields); } public Map> keys() { if (keys.isEmpty()) { return Collections.>emptyMap(); } return Collections.unmodifiableMap(keys); } public DynamoDBMapperTableModel build() { 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; } } } }