/* * Copyright 2013 the original author or authors. * * 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://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.socialsignin.spring.data.dynamodb.repository.query; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import org.socialsignin.spring.data.dynamodb.core.DynamoDBOperations; import org.socialsignin.spring.data.dynamodb.query.CountByHashAndRangeKeyQuery; import org.socialsignin.spring.data.dynamodb.query.MultipleEntityQueryExpressionQuery; import org.socialsignin.spring.data.dynamodb.query.MultipleEntityQueryRequestQuery; import org.socialsignin.spring.data.dynamodb.query.MultipleEntityScanExpressionQuery; import org.socialsignin.spring.data.dynamodb.query.Query; import org.socialsignin.spring.data.dynamodb.query.QueryExpressionCountQuery; import org.socialsignin.spring.data.dynamodb.query.QueryRequestCountQuery; import org.socialsignin.spring.data.dynamodb.query.ScanExpressionCountQuery; import org.socialsignin.spring.data.dynamodb.query.SingleEntityLoadByHashAndRangeKeyQuery; import org.socialsignin.spring.data.dynamodb.repository.support.DynamoDBIdIsHashAndRangeKeyEntityInformation; import org.springframework.util.Assert; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBQueryExpression; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBScanExpression; import com.amazonaws.services.dynamodbv2.model.ComparisonOperator; import com.amazonaws.services.dynamodbv2.model.Condition; import com.amazonaws.services.dynamodbv2.model.QueryRequest; /** * @author Michael Lavelle */ public class DynamoDBEntityWithHashAndRangeKeyCriteria<T, ID extends Serializable> extends AbstractDynamoDBQueryCriteria<T, ID> { private Object rangeKeyAttributeValue; private Object rangeKeyPropertyValue; private String rangeKeyPropertyName; private Set<String> indexRangeKeyPropertyNames; private DynamoDBIdIsHashAndRangeKeyEntityInformation<T, ID> entityInformation; protected String getRangeKeyAttributeName() { return getAttributeName(getRangeKeyPropertyName()); } protected String getRangeKeyPropertyName() { return rangeKeyPropertyName; } protected boolean isRangeKeyProperty(String propertyName) { return rangeKeyPropertyName.equals(propertyName); } public DynamoDBEntityWithHashAndRangeKeyCriteria(DynamoDBIdIsHashAndRangeKeyEntityInformation<T, ID> entityInformation) { super(entityInformation); this.rangeKeyPropertyName = entityInformation.getRangeKeyPropertyName(); this.indexRangeKeyPropertyNames = entityInformation.getIndexRangeKeyPropertyNames(); if (indexRangeKeyPropertyNames == null) { indexRangeKeyPropertyNames = new HashSet<String>(); } this.entityInformation = entityInformation; } public Set<String> getIndexRangeKeyAttributeNames() { Set<String> indexRangeKeyAttributeNames = new HashSet<String>(); for (String indexRangeKeyPropertyName : indexRangeKeyPropertyNames) { indexRangeKeyAttributeNames.add(getAttributeName(indexRangeKeyPropertyName)); } return indexRangeKeyAttributeNames; } protected Object getRangeKeyAttributeValue() { return rangeKeyAttributeValue; } protected Object getRangeKeyPropertyValue() { return rangeKeyPropertyValue; } protected boolean isRangeKeySpecified() { return getRangeKeyAttributeValue() != null; } protected Query<T> buildSingleEntityLoadQuery(DynamoDBOperations dynamoDBOperations) { return new SingleEntityLoadByHashAndRangeKeyQuery<T>(dynamoDBOperations, entityInformation.getJavaType(), getHashKeyPropertyValue(), getRangeKeyPropertyValue()); } protected Query<Long> buildSingleEntityCountQuery(DynamoDBOperations dynamoDBOperations) { return new CountByHashAndRangeKeyQuery<T>(dynamoDBOperations, entityInformation.getJavaType(), getHashKeyPropertyValue(), getRangeKeyPropertyValue()); } private void checkComparisonOperatorPermittedForCompositeHashAndRangeKey(ComparisonOperator comparisonOperator) { if (!ComparisonOperator.EQ.equals(comparisonOperator) && !ComparisonOperator.CONTAINS.equals(comparisonOperator) && !ComparisonOperator.BEGINS_WITH.equals(comparisonOperator)) { throw new UnsupportedOperationException("Only EQ,CONTAINS,BEGINS_WITH supported for composite id comparison"); } } @SuppressWarnings("unchecked") @Override public DynamoDBQueryCriteria<T, ID> withSingleValueCriteria(String propertyName, ComparisonOperator comparisonOperator, Object value, Class<?> propertyType) { if (entityInformation.isCompositeHashAndRangeKeyProperty(propertyName)) { checkComparisonOperatorPermittedForCompositeHashAndRangeKey(comparisonOperator); Object hashKey = entityInformation.getHashKey((ID) value); Object rangeKey = entityInformation.getRangeKey((ID) value); if (hashKey != null) { withSingleValueCriteria(getHashKeyPropertyName(), comparisonOperator, hashKey, hashKey.getClass()); } if (rangeKey != null) { withSingleValueCriteria(getRangeKeyPropertyName(), comparisonOperator, rangeKey, rangeKey.getClass()); } return this; } else { return super.withSingleValueCriteria(propertyName, comparisonOperator, value, propertyType); } } public DynamoDBQueryExpression<T> buildQueryExpression() { DynamoDBQueryExpression<T> queryExpression = new DynamoDBQueryExpression<T>(); if (isHashKeySpecified()) { T hashKeyPrototype = entityInformation.getHashKeyPropotypeEntityForHashKey(getHashKeyPropertyValue()); queryExpression.withHashKeyValues(hashKeyPrototype); queryExpression.withRangeKeyConditions(new HashMap<String, Condition>()); } if (isRangeKeySpecified() && !isApplicableForGlobalSecondaryIndex()) { Condition rangeKeyCondition = createSingleValueCondition(getRangeKeyPropertyName(), ComparisonOperator.EQ, getRangeKeyAttributeValue(), getRangeKeyAttributeValue().getClass(), true); queryExpression.withRangeKeyCondition(getRangeKeyAttributeName(), rangeKeyCondition); applySortIfSpecified(queryExpression, Arrays.asList(new String[] { getRangeKeyPropertyName() })); } else if (isOnlyASingleAttributeConditionAndItIsOnEitherRangeOrIndexRangeKey() || (isApplicableForGlobalSecondaryIndex())) { Entry<String, List<Condition>> singlePropertyConditions = propertyConditions.entrySet().iterator().next(); List<String> allowedSortProperties = new ArrayList<String>(); for (Entry<String, List<Condition>> singlePropertyCondition : propertyConditions.entrySet()) { if (entityInformation.getGlobalSecondaryIndexNamesByPropertyName().keySet() .contains(singlePropertyCondition.getKey())) { allowedSortProperties.add(singlePropertyCondition.getKey()); } } if (allowedSortProperties.size() == 0) { allowedSortProperties.add(singlePropertyConditions.getKey()); } for (Entry<String, List<Condition>> singleAttributeConditions : attributeConditions.entrySet()) { for (Condition condition : singleAttributeConditions.getValue()) { queryExpression.withRangeKeyCondition(singleAttributeConditions.getKey(), condition); } } applySortIfSpecified(queryExpression, allowedSortProperties); if (getGlobalSecondaryIndexName() != null) { queryExpression.setIndexName(getGlobalSecondaryIndexName()); } } else { applySortIfSpecified(queryExpression, Arrays.asList(new String[] { getRangeKeyPropertyName() })); } return queryExpression; } protected List<Condition> getRangeKeyConditions() { List<Condition> rangeKeyConditions = null; if (isApplicableForGlobalSecondaryIndex() && entityInformation.getGlobalSecondaryIndexNamesByPropertyName().keySet().contains(getRangeKeyPropertyName())) { rangeKeyConditions = getRangeKeyAttributeValue() == null ? null : Arrays.asList(createSingleValueCondition( getRangeKeyPropertyName(), ComparisonOperator.EQ, getRangeKeyAttributeValue(), getRangeKeyAttributeValue() .getClass(), true)); } return rangeKeyConditions; } protected Query<T> buildFinderQuery(DynamoDBOperations dynamoDBOperations) { if (isApplicableForQuery()) { if (isApplicableForGlobalSecondaryIndex()) { String tableName = dynamoDBOperations.getOverriddenTableName(entityInformation.getDynamoDBTableName()); QueryRequest queryRequest = buildQueryRequest(tableName, getGlobalSecondaryIndexName(), getHashKeyAttributeName(), getRangeKeyAttributeName(), this.getRangeKeyPropertyName(), getHashKeyConditions(), getRangeKeyConditions()); return new MultipleEntityQueryRequestQuery<T>(dynamoDBOperations,entityInformation.getJavaType(), queryRequest); } else { DynamoDBQueryExpression<T> queryExpression = buildQueryExpression(); return new MultipleEntityQueryExpressionQuery<T>(dynamoDBOperations, entityInformation.getJavaType(), queryExpression); } } else { return new MultipleEntityScanExpressionQuery<T>(dynamoDBOperations, clazz, buildScanExpression()); } } protected Query<Long> buildFinderCountQuery(DynamoDBOperations dynamoDBOperations,boolean pageQuery) { if (isApplicableForQuery()) { if (isApplicableForGlobalSecondaryIndex()) { String tableName = dynamoDBOperations.getOverriddenTableName(entityInformation.getDynamoDBTableName()); QueryRequest queryRequest = buildQueryRequest(tableName, getGlobalSecondaryIndexName(), getHashKeyAttributeName(), getRangeKeyAttributeName(), this.getRangeKeyPropertyName(), getHashKeyConditions(), getRangeKeyConditions()); return new QueryRequestCountQuery<T>(dynamoDBOperations,entityInformation.getJavaType(), queryRequest); } else { DynamoDBQueryExpression<T> queryExpression = buildQueryExpression(); return new QueryExpressionCountQuery<T>(dynamoDBOperations, entityInformation.getJavaType(), queryExpression); } } else { return new ScanExpressionCountQuery<T>(dynamoDBOperations, clazz, buildScanExpression(),pageQuery); } } @Override public boolean isApplicableForLoad() { return attributeConditions.size() == 0 && isHashAndRangeKeySpecified(); } protected boolean isHashAndRangeKeySpecified() { return isHashKeySpecified() && isRangeKeySpecified(); } protected boolean isOnlyASingleAttributeConditionAndItIsOnEitherRangeOrIndexRangeKey() { boolean isOnlyASingleAttributeConditionAndItIsOnEitherRangeOrIndexRangeKey = false; if (!isRangeKeySpecified() && attributeConditions.size() == 1) { Entry<String, List<Condition>> conditionsEntry = attributeConditions.entrySet().iterator().next(); if (conditionsEntry.getKey().equals(getRangeKeyAttributeName()) || getIndexRangeKeyAttributeNames().contains(conditionsEntry.getKey())) { if (conditionsEntry.getValue().size() == 1) { isOnlyASingleAttributeConditionAndItIsOnEitherRangeOrIndexRangeKey = true; } } } return isOnlyASingleAttributeConditionAndItIsOnEitherRangeOrIndexRangeKey; } @Override protected boolean hasIndexHashKeyEqualCondition() { boolean hasCondition = super.hasIndexHashKeyEqualCondition(); if (!hasCondition) { if (rangeKeyAttributeValue != null && entityInformation.isGlobalIndexHashKeyProperty(rangeKeyPropertyName)) { hasCondition = true; } } return hasCondition; } @Override protected boolean hasIndexRangeKeyCondition() { boolean hasCondition = super.hasIndexRangeKeyCondition(); if (!hasCondition) { if (rangeKeyAttributeValue != null && entityInformation.isGlobalIndexRangeKeyProperty(rangeKeyPropertyName)) { hasCondition = true; } } return hasCondition; } protected boolean isApplicableForGlobalSecondaryIndex() { boolean global = super.isApplicableForGlobalSecondaryIndex(); if (global && getRangeKeyAttributeValue() != null && !entityInformation.getGlobalSecondaryIndexNamesByPropertyName().keySet().contains(getRangeKeyPropertyName())) { return false; } return global; } protected String getGlobalSecondaryIndexName() { // Get the target global secondary index name using the property // conditions String globalSecondaryIndexName = super.getGlobalSecondaryIndexName(); // Hash and Range Entities store range key equals conditions as // rangeKeyAttributeValue attribute instead of as property condition // Check this attribute and if specified in the query conditions and // it's the only global secondary index range candidate, // then set the index range key to be that associated with the range key if (globalSecondaryIndexName == null) { if (this.hashKeyAttributeValue == null && getRangeKeyAttributeValue() != null) { String[] rangeKeyIndexNames = entityInformation.getGlobalSecondaryIndexNamesByPropertyName().get( this.getRangeKeyPropertyName()); globalSecondaryIndexName = rangeKeyIndexNames != null && rangeKeyIndexNames.length > 0 ? rangeKeyIndexNames[0] : null; } } return globalSecondaryIndexName; } public boolean isApplicableForQuery() { return isOnlyHashKeySpecified() || (isHashKeySpecified() && isOnlyASingleAttributeConditionAndItIsOnEitherRangeOrIndexRangeKey() && comparisonOperatorsPermittedForQuery()) || isApplicableForGlobalSecondaryIndex(); } public DynamoDBScanExpression buildScanExpression() { if (sort != null) { throw new UnsupportedOperationException("Sort not supported for scan expressions"); } DynamoDBScanExpression scanExpression = new DynamoDBScanExpression(); if (isHashKeySpecified()) { scanExpression.addFilterCondition( getHashKeyAttributeName(), createSingleValueCondition(getHashKeyPropertyName(), ComparisonOperator.EQ, getHashKeyAttributeValue(), getHashKeyAttributeValue().getClass(), true)); } if (isRangeKeySpecified()) { scanExpression.addFilterCondition( getRangeKeyAttributeName(), createSingleValueCondition(getRangeKeyPropertyName(), ComparisonOperator.EQ, getRangeKeyAttributeValue(), getRangeKeyAttributeValue().getClass(), true)); } for (Map.Entry<String, List<Condition>> conditionEntry : attributeConditions.entrySet()) { for (Condition condition : conditionEntry.getValue()) { scanExpression.addFilterCondition(conditionEntry.getKey(), condition); } } return scanExpression; } public DynamoDBQueryCriteria<T, ID> withRangeKeyEquals(Object value) { Assert.notNull(value, "Creating conditions on null range keys not supported: please specify a value for '" + getRangeKeyPropertyName() + "'"); rangeKeyAttributeValue = getPropertyAttributeValue(getRangeKeyPropertyName(), value); rangeKeyPropertyValue = value; return this; } @SuppressWarnings("unchecked") @Override public DynamoDBQueryCriteria<T, ID> withPropertyEquals(String propertyName, Object value, Class<?> propertyType) { if (isHashKeyProperty(propertyName)) { return withHashKeyEquals(value); } else if (isRangeKeyProperty(propertyName)) { return withRangeKeyEquals(value); } else if (entityInformation.isCompositeHashAndRangeKeyProperty(propertyName)) { Assert.notNull(value, "Creating conditions on null composite id properties not supported: please specify a value for '" + propertyName + "'"); Object hashKey = entityInformation.getHashKey((ID) value); Object rangeKey = entityInformation.getRangeKey((ID) value); if (hashKey != null) { withHashKeyEquals(hashKey); } if (rangeKey != null) { withRangeKeyEquals(rangeKey); } return this; } else { Condition condition = createSingleValueCondition(propertyName, ComparisonOperator.EQ, value, propertyType, false); return withCondition(propertyName, condition); } } @Override protected boolean isOnlyHashKeySpecified() { return isHashKeySpecified() && attributeConditions.size() == 0 && !isRangeKeySpecified(); } }