/*
* 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;
}
}