/* * 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.Collection; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.socialsignin.spring.data.dynamodb.core.DynamoDBOperations; import org.socialsignin.spring.data.dynamodb.mapping.DefaultDynamoDBDateMarshaller; import org.socialsignin.spring.data.dynamodb.query.Query; import org.socialsignin.spring.data.dynamodb.repository.support.DynamoDBEntityInformation; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.domain.Sort.Order; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMarshaller; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBQueryExpression; import com.amazonaws.services.dynamodbv2.model.AttributeValue; import com.amazonaws.services.dynamodbv2.model.ComparisonOperator; import com.amazonaws.services.dynamodbv2.model.Condition; import com.amazonaws.services.dynamodbv2.model.QueryRequest; import com.amazonaws.services.dynamodbv2.model.Select; /** * @author Michael Lavelle */ public abstract class AbstractDynamoDBQueryCriteria<T, ID extends Serializable> implements DynamoDBQueryCriteria<T, ID> { protected Class<T> clazz; private DynamoDBEntityInformation<T, ID> entityInformation; private Map<String, String> attributeNamesByPropertyName; private String hashKeyPropertyName; protected MultiValueMap<String, Condition> attributeConditions; protected MultiValueMap<String, Condition> propertyConditions; protected Object hashKeyAttributeValue; protected Object hashKeyPropertyValue; protected String globalSecondaryIndexName; protected Sort sort; public abstract boolean isApplicableForLoad(); protected QueryRequest buildQueryRequest(String tableName, String theIndexName, String hashKeyAttributeName, String rangeKeyAttributeName, String rangeKeyPropertyName, List<Condition> hashKeyConditions, List<Condition> rangeKeyConditions) { // TODO Set other query request properties based on config QueryRequest queryRequest = new QueryRequest(); queryRequest.setTableName(tableName); queryRequest.setIndexName(theIndexName); if (isApplicableForGlobalSecondaryIndex()) { 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()); } } HashMap<String, Condition> keyConditions = new HashMap<String, Condition>(); if (hashKeyConditions != null && hashKeyConditions.size() > 0) { for (Condition hashKeyCondition : hashKeyConditions) { keyConditions.put(hashKeyAttributeName, hashKeyCondition); allowedSortProperties.add(hashKeyPropertyName); } } if (rangeKeyConditions != null && rangeKeyConditions.size() > 0) { for (Condition rangeKeyCondition : rangeKeyConditions) { keyConditions.put(rangeKeyAttributeName, rangeKeyCondition); allowedSortProperties.add(rangeKeyPropertyName); } } for (Entry<String, List<Condition>> singleAttributeConditions : attributeConditions.entrySet()) { for (Condition condition : singleAttributeConditions.getValue()) { keyConditions.put(singleAttributeConditions.getKey(), condition); } } queryRequest.setKeyConditions(keyConditions); queryRequest.setSelect(Select.ALL_PROJECTED_ATTRIBUTES); applySortIfSpecified(queryRequest, new ArrayList<String>(new HashSet<String>(allowedSortProperties))); } return queryRequest; } protected void applySortIfSpecified(DynamoDBQueryExpression<T> queryExpression, List<String> permittedPropertyNames) { if (permittedPropertyNames.size() > 1) { throw new UnsupportedOperationException("Can only sort by at most a single range or index range key"); } if (sort != null) { boolean sortAlreadySet = false; for (Order order : sort) { if (permittedPropertyNames.contains(order.getProperty())) { if (sortAlreadySet) { throw new UnsupportedOperationException("Sorting by multiple attributes not possible"); } queryExpression.setScanIndexForward(order.getDirection().equals(Direction.ASC)); sortAlreadySet = true; } else { throw new UnsupportedOperationException("Sorting only possible by " + permittedPropertyNames + " for the criteria specified"); } } } } protected void applySortIfSpecified(QueryRequest queryRequest, List<String> permittedPropertyNames) { if (permittedPropertyNames.size() > 2) { throw new UnsupportedOperationException("Can only sort by at most a single global hash and range key"); } if (sort != null) { boolean sortAlreadySet = false; for (Order order : sort) { if (permittedPropertyNames.contains(order.getProperty())) { if (sortAlreadySet) { throw new UnsupportedOperationException("Sorting by multiple attributes not possible"); } if (queryRequest.getKeyConditions().size() > 1 && !hasIndexHashKeyEqualCondition()) { throw new UnsupportedOperationException( "Sorting for global index queries with criteria on both hash and range not possible"); } queryRequest.setScanIndexForward(order.getDirection().equals(Direction.ASC)); sortAlreadySet = true; } else { throw new UnsupportedOperationException("Sorting only possible by " + permittedPropertyNames + " for the criteria specified"); } } } } public boolean comparisonOperatorsPermittedForQuery() { List<ComparisonOperator> comparisonOperatorsPermittedForQuery = Arrays.asList(new ComparisonOperator[] { ComparisonOperator.EQ, ComparisonOperator.LE, ComparisonOperator.LT, ComparisonOperator.GE, ComparisonOperator.GT, ComparisonOperator.BEGINS_WITH, ComparisonOperator.BETWEEN }); // Can only query on subset of Conditions for (Collection<Condition> conditions : attributeConditions.values()) { for (Condition condition : conditions) { if (!comparisonOperatorsPermittedForQuery .contains(ComparisonOperator.fromValue(condition.getComparisonOperator()))) { return false; } } } return true; } protected List<Condition> getHashKeyConditions() { List<Condition> hashKeyConditions = null; if (isApplicableForGlobalSecondaryIndex() && entityInformation.getGlobalSecondaryIndexNamesByPropertyName().keySet().contains(getHashKeyPropertyName())) { hashKeyConditions = getHashKeyAttributeValue() == null ? null : Arrays.asList(createSingleValueCondition( getHashKeyPropertyName(), ComparisonOperator.EQ, getHashKeyAttributeValue(), getHashKeyAttributeValue() .getClass(), true)); if (hashKeyConditions == null) { if (attributeConditions.containsKey(getHashKeyAttributeName())) { hashKeyConditions = attributeConditions.get(getHashKeyAttributeName()); } } } return hashKeyConditions; } public AbstractDynamoDBQueryCriteria(DynamoDBEntityInformation<T, ID> dynamoDBEntityInformation) { this.clazz = dynamoDBEntityInformation.getJavaType(); this.attributeConditions = new LinkedMultiValueMap<String, Condition>(); this.propertyConditions = new LinkedMultiValueMap<String, Condition>(); this.hashKeyPropertyName = dynamoDBEntityInformation.getHashKeyPropertyName(); this.entityInformation = dynamoDBEntityInformation; this.attributeNamesByPropertyName = new HashMap<String, String>(); } private String getFirstDeclaredIndexNameForAttribute(Map<String,String[]> indexNamesByAttributeName,List<String> indexNamesToCheck,String attributeName) { String indexName = null; String[] declaredOrderedIndexNamesForAttribute = indexNamesByAttributeName.get(attributeName); for (String declaredOrderedIndexNameForAttribute : declaredOrderedIndexNamesForAttribute) { if (indexName == null && indexNamesToCheck.contains(declaredOrderedIndexNameForAttribute)) { indexName = declaredOrderedIndexNameForAttribute; } } return indexName; } protected String getGlobalSecondaryIndexName() { // Lazy evaluate the globalSecondaryIndexName if not already set // We must have attribute conditions specified in order to use a global secondary index, otherwise return null for index name // Also this method only evaluates the if (globalSecondaryIndexName == null && attributeConditions != null && !attributeConditions.isEmpty()) { // Declare map of index names by attribute name which we will populate below - this will be used to determine which index to use if multiple indexes are applicable Map<String,String[]> indexNamesByAttributeName = new HashMap<String,String[]>(); // Declare map of attribute lists by index name which we will populate below - this will be used to determine whether we have an exact match index for specified attribute conditions MultiValueMap<String,String> attributeListsByIndexName = new LinkedMultiValueMap<String,String>(); // Populate the above maps for (Entry<String, String[]> indexNamesForPropertyNameEntry : entityInformation.getGlobalSecondaryIndexNamesByPropertyName().entrySet()) { String propertyName = indexNamesForPropertyNameEntry.getKey(); String attributeName = getAttributeName(propertyName); indexNamesByAttributeName.put(attributeName, indexNamesForPropertyNameEntry.getValue()); for (String indexNameForPropertyName : indexNamesForPropertyNameEntry.getValue()) { attributeListsByIndexName.add(indexNameForPropertyName, attributeName); } } // Declare lists to store matching index names List<String> exactMatchIndexNames = new ArrayList<String>(); List<String> partialMatchIndexNames = new ArrayList<String>(); // Populate matching index name lists - an index is either an exact match ( the index attributes match all the specified criteria exactly) // or a partial match ( the properties for the specified criteria are contained within the property set for an index ) for (Entry<String, List<String>> attributeListForIndexNameEntry : attributeListsByIndexName.entrySet()) { String indexNameForAttributeList = attributeListForIndexNameEntry.getKey(); List<String> attributeList = attributeListForIndexNameEntry.getValue(); if (attributeList.containsAll(attributeConditions.keySet())) { if (attributeConditions.keySet().containsAll(attributeList)) { exactMatchIndexNames.add(indexNameForAttributeList); } else { partialMatchIndexNames.add(indexNameForAttributeList); } } } if (exactMatchIndexNames.size() > 1) { throw new RuntimeException("Multiple indexes defined on same attribute set:" + attributeConditions.keySet()); } else if (exactMatchIndexNames.size() == 1) { globalSecondaryIndexName = exactMatchIndexNames.get(0); } else if (partialMatchIndexNames.size() > 1) { if (attributeConditions.size() == 1) { globalSecondaryIndexName = getFirstDeclaredIndexNameForAttribute(indexNamesByAttributeName, partialMatchIndexNames, attributeConditions.keySet().iterator().next()); } if (globalSecondaryIndexName == null) { globalSecondaryIndexName = partialMatchIndexNames.get(0); } } else if (partialMatchIndexNames.size() == 1) { globalSecondaryIndexName = partialMatchIndexNames.get(0); } } return globalSecondaryIndexName; } protected boolean isHashKeyProperty(String propertyName) { return hashKeyPropertyName.equals(propertyName); } protected String getHashKeyPropertyName() { return hashKeyPropertyName; } protected String getHashKeyAttributeName() { return getAttributeName(getHashKeyPropertyName()); } protected boolean hasIndexHashKeyEqualCondition() { boolean hasIndexHashKeyEqualCondition = false; for (Map.Entry<String, List<Condition>> propertyConditionList : propertyConditions.entrySet()) { if (entityInformation.isGlobalIndexHashKeyProperty(propertyConditionList.getKey())) { for (Condition condition : propertyConditionList.getValue()) { if ( condition.getComparisonOperator().equals(ComparisonOperator.EQ.name())) { hasIndexHashKeyEqualCondition = true; } } } } if (hashKeyAttributeValue != null && entityInformation.isGlobalIndexHashKeyProperty(hashKeyPropertyName)) { hasIndexHashKeyEqualCondition = true; } return hasIndexHashKeyEqualCondition; } protected boolean hasIndexRangeKeyCondition() { boolean hasIndexRangeKeyCondition = false; for (Map.Entry<String, List<Condition>> propertyConditionList : propertyConditions.entrySet()) { if (entityInformation.isGlobalIndexRangeKeyProperty(propertyConditionList.getKey())) { hasIndexRangeKeyCondition = true; } } if (hashKeyAttributeValue != null && entityInformation.isGlobalIndexRangeKeyProperty(hashKeyPropertyName)) { hasIndexRangeKeyCondition = true; } return hasIndexRangeKeyCondition; } protected boolean isApplicableForGlobalSecondaryIndex() { boolean global = this.getGlobalSecondaryIndexName() != null; if (global && getHashKeyAttributeValue() != null && !entityInformation.getGlobalSecondaryIndexNamesByPropertyName().keySet().contains(getHashKeyPropertyName())) { return false; } int attributeConditionCount = attributeConditions.keySet().size(); boolean attributeConditionsAppropriate = hasIndexHashKeyEqualCondition() && (attributeConditionCount == 1 || (attributeConditionCount == 2 && hasIndexRangeKeyCondition())); return global && (attributeConditionCount == 0 || attributeConditionsAppropriate) && comparisonOperatorsPermittedForQuery(); } public DynamoDBQueryCriteria<T, ID> withHashKeyEquals(Object value) { Assert.notNull(value, "Creating conditions on null hash keys not supported: please specify a value for '" + getHashKeyPropertyName() + "'"); hashKeyAttributeValue = getPropertyAttributeValue(getHashKeyPropertyName(), value); hashKeyPropertyValue = value; return this; } public boolean isHashKeySpecified() { return getHashKeyAttributeValue() != null; } public Object getHashKeyAttributeValue() { return hashKeyAttributeValue; } public Object getHashKeyPropertyValue() { return hashKeyPropertyValue; } protected String getAttributeName(String propertyName) { String attributeName = attributeNamesByPropertyName.get(propertyName); if (attributeName == null) { String overriddenName = entityInformation.getOverriddenAttributeName(propertyName); attributeName = overriddenName != null ? overriddenName : propertyName; attributeNamesByPropertyName.put(propertyName, attributeName); } return attributeName; } @Override public DynamoDBQueryCriteria<T, ID> withPropertyBetween(String propertyName, Object value1, Object value2, Class<?> type) { Condition condition = createCollectionCondition(propertyName, ComparisonOperator.BETWEEN, Arrays.asList(value1, value2), type); return withCondition(propertyName, condition); } @Override public DynamoDBQueryCriteria<T, ID> withPropertyIn(String propertyName, Iterable<?> value, Class<?> propertyType) { Condition condition = createCollectionCondition(propertyName, ComparisonOperator.IN, value, propertyType); return withCondition(propertyName, condition); } @Override public DynamoDBQueryCriteria<T, ID> withSingleValueCriteria(String propertyName, ComparisonOperator comparisonOperator, Object value, Class<?> propertyType) { if (comparisonOperator.equals(ComparisonOperator.EQ)) { return withPropertyEquals(propertyName, value, propertyType); } else { Condition condition = createSingleValueCondition(propertyName, comparisonOperator, value, propertyType, false); return withCondition(propertyName, condition); } } @Override public Query<T> buildQuery(DynamoDBOperations dynamoDBOperations) { if (isApplicableForLoad()) { return buildSingleEntityLoadQuery(dynamoDBOperations); } else { return buildFinderQuery(dynamoDBOperations); } } @Override public Query<Long> buildCountQuery(DynamoDBOperations dynamoDBOperations,boolean pageQuery) { if (isApplicableForLoad()) { return buildSingleEntityCountQuery(dynamoDBOperations); } else { return buildFinderCountQuery(dynamoDBOperations,pageQuery); } } protected abstract Query<T> buildSingleEntityLoadQuery(DynamoDBOperations dynamoDBOperations); protected abstract Query<Long> buildSingleEntityCountQuery(DynamoDBOperations dynamoDBOperations); protected abstract Query<T> buildFinderQuery(DynamoDBOperations dynamoDBOperations); protected abstract Query<Long> buildFinderCountQuery(DynamoDBOperations dynamoDBOperations,boolean pageQuery); protected abstract boolean isOnlyHashKeySpecified(); @Override public DynamoDBQueryCriteria<T, ID> withNoValuedCriteria(String propertyName, ComparisonOperator comparisonOperator) { Condition condition = createNoValueCondition(propertyName, comparisonOperator); return withCondition(propertyName, condition); } public DynamoDBQueryCriteria<T, ID> withCondition(String propertyName, Condition condition) { attributeConditions.add(getAttributeName(propertyName), condition); propertyConditions.add(propertyName, condition); return this; } @SuppressWarnings("unchecked") protected <V> Object getPropertyAttributeValue(String propertyName, Object value) { DynamoDBMarshaller<V> marshaller = (DynamoDBMarshaller<V>) entityInformation.getMarshallerForProperty(propertyName); if (marshaller != null) { return marshaller.marshall((V) value); } else { return value; } } protected <V> Condition createNoValueCondition(String propertyName, ComparisonOperator comparisonOperator) { Condition condition = new Condition().withComparisonOperator(comparisonOperator); return condition; } private List<String> getNumberListAsStringList(List<Number> numberList) { List<String> list = new ArrayList<String>(); for (Number number : numberList) { if (number != null) { list.add(number.toString()); } else { list.add(null); } } return list; } private List<String> getDateListAsStringList(List<Date> dateList) { DynamoDBMarshaller<Date> marshaller = new DefaultDynamoDBDateMarshaller(); List<String> list = new ArrayList<String>(); for (Date date : dateList) { if (date != null) { list.add(marshaller.marshall(date)); } else { list.add(null); } } return list; } private List<String> getBooleanListAsStringList(List<Boolean> booleanList) { List<String> list = new ArrayList<String>(); for (Boolean booleanValue : booleanList) { if (booleanValue != null) { list.add(booleanValue.booleanValue() ? "1" : "0"); } else { list.add(null); } } return list; } @SuppressWarnings("unchecked") private <P> List<P> getAttributeValueAsList(Object attributeValue) { boolean isIterable = ClassUtils.isAssignable(Iterable.class, attributeValue.getClass()); List<P> attributeValueAsList = null; if (isIterable) { attributeValueAsList = new ArrayList<P>(); Iterable<P> iterable = (Iterable<P>) attributeValue; for (P attributeValueElement : iterable) { attributeValueAsList.add(attributeValueElement); } return attributeValueAsList; } return null; } protected <P> List<AttributeValue> addAttributeValue(List<AttributeValue> attributeValueList, Object attributeValue, String propertyName, Class<P> propertyType, boolean expandCollectionValues) { AttributeValue attributeValueObject = new AttributeValue(); if (ClassUtils.isAssignable(String.class, propertyType)) { List<String> attributeValueAsList = getAttributeValueAsList(attributeValue); if (expandCollectionValues && attributeValueAsList != null) { attributeValueObject.withSS(attributeValueAsList); } else { attributeValueObject.withS((String) attributeValue); } } else if (ClassUtils.isAssignable(Number.class, propertyType)) { List<Number> attributeValueAsList = getAttributeValueAsList(attributeValue); if (expandCollectionValues && attributeValueAsList != null) { List<String> attributeValueAsStringList = getNumberListAsStringList(attributeValueAsList); attributeValueObject.withNS(attributeValueAsStringList); } else { attributeValueObject.withN(attributeValue.toString()); } } else if (ClassUtils.isAssignable(Boolean.class, propertyType)) { List<Boolean> attributeValueAsList = getAttributeValueAsList(attributeValue); if (expandCollectionValues && attributeValueAsList != null) { List<String> attributeValueAsStringList = getBooleanListAsStringList(attributeValueAsList); attributeValueObject.withNS(attributeValueAsStringList); } else { boolean boolValue = ((Boolean) attributeValue).booleanValue(); attributeValueObject.withN(boolValue ? "1" : "0"); } } else if (ClassUtils.isAssignable(Date.class, propertyType)) { List<Date> attributeValueAsList = getAttributeValueAsList(attributeValue); if (expandCollectionValues && attributeValueAsList != null) { List<String> attributeValueAsStringList = getDateListAsStringList(attributeValueAsList); attributeValueObject.withSS(attributeValueAsStringList); } else { Date date = (Date) attributeValue; String marshalledDate = new DefaultDynamoDBDateMarshaller().marshall(date); attributeValueObject.withS(marshalledDate); } } else { throw new RuntimeException("Cannot create condition for type:" + attributeValue.getClass() + " property conditions must be String,Number or Boolean, or have a DynamoDBMarshaller configured"); } attributeValueList.add(attributeValueObject); return attributeValueList; } protected Condition createSingleValueCondition(String propertyName, ComparisonOperator comparisonOperator, Object o, Class<?> propertyType, boolean alreadyMarshalledIfRequired) { Assert.notNull(o, "Creating conditions on null property values not supported: please specify a value for '" + propertyName + "'"); Object attributeValue = !alreadyMarshalledIfRequired ? getPropertyAttributeValue(propertyName, o) : o; boolean marshalled = !alreadyMarshalledIfRequired && attributeValue != o && !entityInformation.isCompositeHashAndRangeKeyProperty(propertyName); Class<?> targetPropertyType = marshalled ? String.class : propertyType; List<AttributeValue> attributeValueList = new ArrayList<AttributeValue>(); attributeValueList = addAttributeValue(attributeValueList, attributeValue, propertyName, targetPropertyType, true); return new Condition().withComparisonOperator(comparisonOperator).withAttributeValueList(attributeValueList); } protected Condition createCollectionCondition(String propertyName, ComparisonOperator comparisonOperator, Iterable<?> o, Class<?> propertyType) { Assert.notNull(o, "Creating conditions on null property values not supported: please specify a value for '" + propertyName + "'"); List<AttributeValue> attributeValueList = new ArrayList<AttributeValue>(); boolean marshalled = false; for (Object object : o) { Object attributeValue = getPropertyAttributeValue(propertyName, object); if (attributeValue != null) { marshalled = attributeValue != object && !entityInformation.isCompositeHashAndRangeKeyProperty(propertyName); } Class<?> targetPropertyType = marshalled ? String.class : propertyType; attributeValueList = addAttributeValue(attributeValueList, attributeValue, propertyName, targetPropertyType, false); } return new Condition().withComparisonOperator(comparisonOperator).withAttributeValueList(attributeValueList); } @Override public DynamoDBQueryCriteria<T, ID> withSort(Sort sort) { this.sort = sort; return this; } }