All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.amazonaws.mobileconnectors.dynamodbv2.dynamodbmapper.DynamoDBTableSchemaParser Maven / Gradle / Ivy

Go to download

The AWS Android SDK for Amazon DynamoDB Mapper module holds the client classes that are used for communicating with Amazon DynamoDB Service

There is a newer version: 2.79.0
Show newest version
/*
 * Copyright 2011-2016 Amazon.com, Inc. or its affiliates. All 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.mobileconnectors.dynamodbv2.dynamodbmapper;

import com.amazonaws.mobileconnectors.dynamodbv2.dynamodbmapper.DynamoDBMapperFieldModel.DynamoDBAttributeType;
import com.amazonaws.services.dynamodbv2.model.AttributeDefinition;
import com.amazonaws.services.dynamodbv2.model.CreateTableRequest;
import com.amazonaws.services.dynamodbv2.model.GlobalSecondaryIndex;
import com.amazonaws.services.dynamodbv2.model.KeySchemaElement;
import com.amazonaws.services.dynamodbv2.model.KeyType;
import com.amazonaws.services.dynamodbv2.model.LocalSecondaryIndex;
import com.amazonaws.services.dynamodbv2.model.Projection;
import com.amazonaws.services.dynamodbv2.model.ProjectionType;

import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;

/**
 * A class responsible for parsing the primary key and index schema of a table
 * POJO.
 */
class DynamoDBTableSchemaParser {

    private final Map, TableIndexesInfo> tableIndexesInfoCache =
            new HashMap, TableIndexesInfo>();

    /**
     * Parse the given POJO class and return the CreateTableRequest for the
     * DynamoDB table it represents. Note that the returned request does not
     * include the required ProvisionedThroughput parameters for the primary
     * table and the GSIs, and that all secondary indexes are initialized with
     * the default projection type - KEY_ONLY.
     *
     * @param clazz The POJO class.
     * @param config The DynamoDBMapperConfig which contains the
     *            TableNameOverrides parameter used to determine the table name.
     * @param reflector The DynamoDBReflector that provides all the relevant
     *            getters of the POJO.
     */
    CreateTableRequest parseTablePojoToCreateTableRequest(
            Class clazz,
            DynamoDBMapperConfig config,
            DynamoDBReflector reflector,
            ItemConverter converter) {

        CreateTableRequest createTableRequest = new CreateTableRequest();
        createTableRequest.setTableName(DynamoDBMapper.internalGetTableName(clazz, null, config));

        // Primary keys
        Method pHashKeyGetter = reflector.getPrimaryHashKeyGetter(clazz);
        AttributeDefinition pHashAttrDefinition = getKeyAttributeDefinition(pHashKeyGetter,
                converter);
        createTableRequest.withKeySchema(new KeySchemaElement(pHashAttrDefinition
                .getAttributeName(), KeyType.HASH));
        // Primary range
        Method pRangeKeyGetter = reflector.getPrimaryRangeKeyGetter(clazz);
        AttributeDefinition pRangeAttrDefinition = null;
        if (pRangeKeyGetter != null) {
            pRangeAttrDefinition = getKeyAttributeDefinition(pRangeKeyGetter, converter);
            createTableRequest.withKeySchema(new KeySchemaElement(pRangeAttrDefinition
                    .getAttributeName(), KeyType.RANGE));
        }

        // Parse the index schema
        TableIndexesInfo indexesInfo = parseTableIndexes(clazz, reflector);
        if (indexesInfo.getGlobalSecondaryIndexes().isEmpty() == false) {
            createTableRequest.setGlobalSecondaryIndexes(indexesInfo.getGlobalSecondaryIndexes());
        }
        if (indexesInfo.getLocalSecondaryIndexes().isEmpty() == false) {
            createTableRequest.setLocalSecondaryIndexes(indexesInfo.getLocalSecondaryIndexes());
        }

        // Aggregate all key attribute definitions
        Map attrDefinitions = new HashMap();
        // Hash key definition
        putAfterCheckConflict(attrDefinitions, pHashAttrDefinition);
        // Range key definition
        if (pRangeKeyGetter != null) {
            putAfterCheckConflict(attrDefinitions, pRangeAttrDefinition);
        }
        for (Method indexKeyGetter : indexesInfo.getIndexKeyGetters()) {
            AttributeDefinition indexKeyAttrDefinition = getKeyAttributeDefinition(indexKeyGetter,
                    converter);
            putAfterCheckConflict(attrDefinitions, indexKeyAttrDefinition);
        }
        createTableRequest.setAttributeDefinitions(attrDefinitions.values());

        return createTableRequest;
    }

    TableIndexesInfo parseTableIndexes(final Class clazz, final DynamoDBReflector reflector) {
        synchronized (tableIndexesInfoCache) {
            if (!tableIndexesInfoCache.containsKey(clazz)) {
                TableIndexesInfo tableIndexInfo = new TableIndexesInfo();
                String pHashName = reflector.getPrimaryHashKeyName(clazz);

                for (Method getter : reflector.getRelevantGetters(clazz)) {
                    // Only consider 0-arg getters
                    if (getter.getParameterTypes().length != 0) {
                        continue;
                    }

                    String attributeName = reflector.getAttributeName(getter);

                    if (ReflectionUtils.getterOrFieldHasAnnotation(getter,
                            DynamoDBIndexHashKey.class)) {
                        DynamoDBIndexHashKey indexHashKeyAnnotation = ReflectionUtils
                                .getAnnotationFromGetterOrField(getter, DynamoDBIndexHashKey.class);
                        String gsiName = indexHashKeyAnnotation.globalSecondaryIndexName();
                        String[] gsiNames = indexHashKeyAnnotation.globalSecondaryIndexNames();

                        boolean singleGsiName = gsiName != null &&
                                gsiName.length() != 0;
                        boolean multipleGsiNames = gsiNames != null &&
                                gsiNames.length != 0;

                        if (singleGsiName && multipleGsiNames) {
                            throw new DynamoDBMappingException(
                                    "@DynamoDBIndexHashKey annotation on getter "
                                            + getter
                                            +
                                            " contains both globalSecondaryIndexName and globalSecondaryIndexNames.");
                        } else if ((!singleGsiName) && (!multipleGsiNames)) {
                            throw new DynamoDBMappingException(
                                    "@DynamoDBIndexHashKey annotation on getter " + getter +
                                            " doesn't contain any index name.");
                        }

                        if (singleGsiName) {
                            tableIndexInfo.addGsiKeys(gsiName, attributeName, null);
                        } else if (multipleGsiNames) {
                            for (String gsi : gsiNames) {
                                tableIndexInfo.addGsiKeys(gsi, attributeName, null);
                            }
                        }
                        tableIndexInfo.addIndexKeyGetter(getter);
                    }

                    if (ReflectionUtils.getterOrFieldHasAnnotation(getter,
                            DynamoDBIndexRangeKey.class)) {
                        DynamoDBIndexRangeKey indexRangeKeyAnnotation = ReflectionUtils
                                .getAnnotationFromGetterOrField(getter, DynamoDBIndexRangeKey.class);
                        String gsiName = indexRangeKeyAnnotation.globalSecondaryIndexName();
                        String[] gsiNames = indexRangeKeyAnnotation.globalSecondaryIndexNames();
                        String lsiName = indexRangeKeyAnnotation.localSecondaryIndexName();
                        String[] lsiNames = indexRangeKeyAnnotation.localSecondaryIndexNames();

                        boolean singleGsiName = gsiName != null &&
                                gsiName.length() != 0;
                        boolean multipleGsiNames = gsiNames != null &&
                                gsiNames.length != 0;
                        boolean singleLsiName = lsiName != null &&
                                lsiName.length() != 0;
                        boolean multipleLsiNames = lsiNames != null &&
                                lsiNames.length != 0;

                        if (singleGsiName && multipleGsiNames) {
                            throw new DynamoDBMappingException(
                                    "@DynamoDBIndexRangeKey annotation on getter "
                                            + getter
                                            +
                                            " contains both globalSecondaryIndexName and globalSecondaryIndexNames.");
                        }
                        if (singleLsiName && multipleLsiNames) {
                            throw new DynamoDBMappingException(
                                    "@DynamoDBIndexRangeKey annotation on getter "
                                            + getter
                                            +
                                            " contains both localSecondaryIndexName and localSecondaryIndexNames.");
                        }
                        if ((!singleGsiName) && (!multipleGsiNames) && (!singleLsiName)
                                && (!multipleLsiNames)) {
                            throw new DynamoDBMappingException(
                                    "@DynamoDBIndexRangeKey annotation on getter " + getter +
                                            " doesn't contain any index name.");
                        }

                        if (singleGsiName) {
                            tableIndexInfo.addGsiKeys(gsiName, null, attributeName);
                        } else if (multipleGsiNames) {
                            for (String gsi : gsiNames) {
                                tableIndexInfo.addGsiKeys(gsi, null, attributeName);
                            }
                        }
                        if (singleLsiName) {
                            tableIndexInfo.addLsiRangeKey(lsiName, pHashName, attributeName);
                        } else if (multipleLsiNames) {
                            for (String lsi : lsiNames) {
                                tableIndexInfo.addLsiRangeKey(lsi, pHashName, attributeName);
                            }
                        }
                        tableIndexInfo.addIndexKeyGetter(getter);
                    }
                } // end of for loop
                tableIndexesInfoCache.put(clazz, tableIndexInfo);
            } // end of the if-cache-does-not-contain block
            return tableIndexesInfoCache.get(clazz);
        } // end of synchronized block
    }

    private static AttributeDefinition getKeyAttributeDefinition(
            Method keyGetter,
            ItemConverter converter) {

        DynamoDBMapperFieldModel fieldModel = converter.getFieldModel(keyGetter);

        String keyAttrName = fieldModel.getDynamoDBAttributeName();
        DynamoDBAttributeType keyType = fieldModel.getDynamoDBAttributeType();

        if (keyType == DynamoDBAttributeType.S ||
                keyType == DynamoDBAttributeType.N ||
                keyType == DynamoDBAttributeType.B) {
            return new AttributeDefinition(keyAttrName, keyType.toString());
        }

        throw new DynamoDBMappingException(
                "The key attribute must be in a scalar type "
                        + "(String, Number or Binary).");
    }

    private static void putAfterCheckConflict(Map map,
            AttributeDefinition attrDefinition) {
        String attrName = attrDefinition.getAttributeName();
        AttributeDefinition existingDefinition = map.get(attrName);
        if (existingDefinition != null && !existingDefinition.equals(attrDefinition)) {
            throw new DynamoDBMappingException(
                    "Found conflicting definitions for attribute [" + attrName + "]: " +
                            existingDefinition + " and " + attrDefinition + ".");
        } else {
            map.put(attrName, attrDefinition);
        }
    }

    /**
     * This class contains all the information about a table's index schema
     * parsed from a table POJO class.
     */
    static class TableIndexesInfo {

        /** Used for mapping an index key name to all the applicable indexes. */
        private final Map> lsiRangeKeyNameToIndexNames =
                new HashMap>();
        private final Map> gsiHashKeyNameToIndexNames =
                new HashMap>();
        private final Map> gsiRangeKeyNameToIndexNames =
                new HashMap>();

        /**
         * Note that the KeySchema in each LocalSecondaryIndex does not include
         * the hash key.
         */
        private final Map lsiNameToLsiDefinition = new HashMap();
        private final Map gsiNameToGsiDefinition = new HashMap();

        /** All getter methods of index key attributes. */
        private final Set indexKeyGetters = new HashSet();

        /**
         * Returns the names of all the annotated local secondary indexes that
         * use the given attribute as the index range key.
         */
        public Set getLsiNamesByIndexRangeKey(String indexRangeKeyName) {
            Set lsiNames = lsiRangeKeyNameToIndexNames.get(indexRangeKeyName);
            if (lsiNames != null) {
                lsiNames = Collections.unmodifiableSet(lsiNames);
            }
            return lsiNames;
        }

        /**
         * Returns the names of all the annotated global secondary indexes that
         * use the given attribute as the index hash key.
         */
        public Set getGsiNamesByIndexHashKey(String indexHashKeyName) {
            Set gsiNames = gsiHashKeyNameToIndexNames.get(indexHashKeyName);
            if (gsiNames != null) {
                gsiNames = Collections.unmodifiableSet(gsiNames);
            }
            return gsiNames;
        }

        /**
         * Returns the names of all the annotated global secondary indexes that
         * use the given attribute as the index range key.
         */
        public Set getGsiNamesByIndexRangeKey(String indexRangeKeyName) {
            Set gsiNames = gsiRangeKeyNameToIndexNames.get(indexRangeKeyName);
            if (gsiNames != null) {
                gsiNames = Collections.unmodifiableSet(gsiNames);
            }
            return gsiNames;
        }

        /**
         * Returns the names of all the annotated local secondary indexes of
         * this POJO class.
         */
        public Set getAllLsiNames() {
            return Collections.unmodifiableSet(lsiNameToLsiDefinition.keySet());
        }

        /**
         * Returns the names of all the annotated global secondary indexes of
         * this POJO class.
         */
        public Set getAllGsiNames() {
            return Collections.unmodifiableSet(gsiNameToGsiDefinition.keySet());
        }

        /*
         * Private interfaces
         */

        private void addGsiKeys(String gsiName, String gsiHashKeyName, String gsiRangeKeyName) {
            GlobalSecondaryIndex gsi;
            if (gsiNameToGsiDefinition.containsKey(gsiName)) {
                GlobalSecondaryIndex existingGsi = gsiNameToGsiDefinition.get(gsiName);
                gsi = existingGsi;

                if (!gsiName.equals(existingGsi.getIndexName())) {
                    throw new IllegalStateException(
                            "Found invalid state of an existing GlobalSecondaryIndex object " +
                                    "associated with the GSI [" + gsiName + "].");
                }

                for (KeySchemaElement existingKey : existingGsi.getKeySchema()) {
                    String existingKeyName = existingKey.getAttributeName();
                    String existingKeyType = existingKey.getKeyType();

                    if (KeyType.HASH.toString().equals(existingKeyType)) {
                        if (gsiHashKeyName != null && !gsiHashKeyName.equals(existingKeyName)) {
                            throw new DynamoDBMappingException("Multiple hash keys ["
                                    + existingKeyName + ", " + gsiHashKeyName +
                                    "] are found for the GSI [" + gsiName + "]. " +
                                    "Each index allows at most one range key attribute.");
                        }
                    } else if (KeyType.RANGE.toString().equals(existingKeyType)) {
                        if (gsiRangeKeyName != null && !gsiRangeKeyName.equals(existingKeyName)) {
                            throw new DynamoDBMappingException("Multiple range keys ["
                                    + existingKeyName + ", " + gsiRangeKeyName +
                                    "] are found for the GSI [" + gsiName + "]. " +
                                    "Each index allows at most one range key attribute.");
                        }
                    } else {
                        // Existing key element is neither HASH nor RANGE.
                        throw new IllegalStateException(
                                "Found invalid state of an existing GlobalSecondaryIndex object " +
                                        "associated with the GSI [" + gsiName + "].");
                    }
                }
            } else {
                gsi = new GlobalSecondaryIndex()
                        .withIndexName(gsiName)
                        .withProjection(
                                new Projection().withProjectionType(ProjectionType.KEYS_ONLY));
                gsiNameToGsiDefinition.put(gsiName, gsi);
            }

            if (gsiHashKeyName != null) {
                // Make sure that the HASH key element is always inserted at the
                // beginning of the list
                if (gsi.getKeySchema() == null || gsi.getKeySchema().isEmpty()) {
                    gsi.withKeySchema(new KeySchemaElement(gsiHashKeyName, KeyType.HASH));
                } else {
                    LinkedList orderedKeys = new LinkedList(
                            gsi.getKeySchema());
                    orderedKeys.addFirst(new KeySchemaElement(gsiHashKeyName, KeyType.HASH));
                    gsi.setKeySchema(orderedKeys);
                }

                // Register the mapping from the hash key name to the GSI name
                mapGsiHashKeyToIndexName(gsiHashKeyName, gsiName);
            }
            if (gsiRangeKeyName != null) {
                gsi.withKeySchema(new KeySchemaElement(gsiRangeKeyName, KeyType.RANGE));

                // Register the mapping from the range key name to the GSI name
                mapGsiRangeKeyToIndexName(gsiRangeKeyName, gsiName);
            }
        }

        private void addLsiRangeKey(String lsiName, String pHashKeyName, String lsiRangeKeyName) {
            if (pHashKeyName == null) {
                throw new IllegalArgumentException(
                        "The name of the primary hash key must be specified.");
            }

            if (lsiNameToLsiDefinition.containsKey(lsiName)) {
                LocalSecondaryIndex existingLsi = lsiNameToLsiDefinition.get(lsiName);
                if (!lsiName.equals(existingLsi.getIndexName())
                        || existingLsi.getKeySchema() == null
                        || existingLsi.getKeySchema().size() != 2 // the hash
                                                                  // key element
                                                                  // should be
                                                                  // already
                                                                  // added
                        || !KeyType.RANGE.toString().equals(
                                existingLsi.getKeySchema().get(1).getKeyType())) {
                    throw new IllegalStateException(
                            "Found invalid state of an existing LocalSecondaryIndex object " +
                                    "associated with the LSI [" + lsiName + "].");
                }

                String existingLsiRangeKeyName = existingLsi.getKeySchema().get(1)
                        .getAttributeName();
                if (!existingLsiRangeKeyName.equals(lsiRangeKeyName)) {
                    throw new DynamoDBMappingException("Multiple range keys ["
                            + existingLsiRangeKeyName + ", " + lsiRangeKeyName +
                            "] are found for the LSI [" + lsiName + "]. " +
                            "Each index allows at most one range key attribute.");
                }
            } else {
                lsiNameToLsiDefinition.put(
                        lsiName,
                        new LocalSecondaryIndex()
                                .withIndexName(lsiName)
                                .withKeySchema(
                                        new KeySchemaElement(pHashKeyName, KeyType.HASH),
                                        new KeySchemaElement(lsiRangeKeyName, KeyType.RANGE))
                                .withProjection(
                                        new Projection()
                                                .withProjectionType(ProjectionType.KEYS_ONLY)));
                mapLsiRangeKeyToIndexName(lsiRangeKeyName, lsiName);
            }
        }

        private void mapLsiRangeKeyToIndexName(String lsiRangeKeyName, String lsiName) {
            mapIndexKeyToIndexName(lsiRangeKeyNameToIndexNames, lsiRangeKeyName, lsiName);
        }

        private void mapGsiHashKeyToIndexName(String gsiHashKeyName, String gsiName) {
            mapIndexKeyToIndexName(gsiHashKeyNameToIndexNames, gsiHashKeyName, gsiName);
        }

        private void mapGsiRangeKeyToIndexName(String gsiRangeKeyName, String gsiName) {
            mapIndexKeyToIndexName(gsiRangeKeyNameToIndexNames, gsiRangeKeyName, gsiName);
        }

        private void mapIndexKeyToIndexName(Map> indexKeyNameToIndexNames,
                String indexKeyName,
                String indexName) {
            if (indexKeyNameToIndexNames.get(indexKeyName) == null) {
                Set indexNames = new HashSet();
                indexNames.add(indexName);
                indexKeyNameToIndexNames.put(indexKeyName, indexNames);
            } else {
                indexKeyNameToIndexNames.get(indexKeyName).add(indexName);
            }
        }

        private void addIndexKeyGetter(Method indexKeyGetter) {
            indexKeyGetters.add(indexKeyGetter);
        }

        private Set getIndexKeyGetters() {
            return Collections.unmodifiableSet(indexKeyGetters);
        }

        private Collection getLocalSecondaryIndexes() {
            return Collections.unmodifiableCollection(lsiNameToLsiDefinition.values());
        }

        private Collection getGlobalSecondaryIndexes() {
            return Collections.unmodifiableCollection(gsiNameToGsiDefinition.values());
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy