/*
* Copyright (c) 2011-2014 Jeppetto and Jonathan Thompson
*
* 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.iternine.jeppetto.dao.dynamodb;
import org.iternine.jeppetto.dao.AccessControlContextProvider;
import org.iternine.jeppetto.dao.Condition;
import org.iternine.jeppetto.dao.ConditionType;
import org.iternine.jeppetto.dao.FailedBatchException;
import org.iternine.jeppetto.dao.JeppettoException;
import org.iternine.jeppetto.dao.NoSuchItemException;
import org.iternine.jeppetto.dao.OptimisticLockException;
import org.iternine.jeppetto.dao.Pair;
import org.iternine.jeppetto.dao.Projection;
import org.iternine.jeppetto.dao.ProjectionType;
import org.iternine.jeppetto.dao.QueryModel;
import org.iternine.jeppetto.dao.QueryModelDAO;
import org.iternine.jeppetto.dao.Sort;
import org.iternine.jeppetto.dao.SortDirection;
import org.iternine.jeppetto.dao.TooManyItemsException;
import org.iternine.jeppetto.dao.UpdateBehaviorDescriptor;
import org.iternine.jeppetto.dao.ResultFromUpdate;
import org.iternine.jeppetto.dao.dynamodb.expression.ConditionExpressionBuilder;
import org.iternine.jeppetto.dao.dynamodb.expression.ProjectionExpressionBuilder;
import org.iternine.jeppetto.dao.dynamodb.expression.UpdateExpressionBuilder;
import org.iternine.jeppetto.dao.dynamodb.iterable.BatchGetIterable;
import org.iternine.jeppetto.dao.dynamodb.iterable.DynamoDBIterable;
import org.iternine.jeppetto.dao.dynamodb.iterable.QueryIterable;
import org.iternine.jeppetto.dao.dynamodb.iterable.ScanIterable;
import org.iternine.jeppetto.dao.id.IdGenerator;
import org.iternine.jeppetto.dao.updateobject.UpdateObject;
import org.iternine.jeppetto.enhance.Enhancer;
import com.amazonaws.AmazonClientException;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
import com.amazonaws.services.dynamodbv2.model.AttributeValue;
import com.amazonaws.services.dynamodbv2.model.BatchGetItemRequest;
import com.amazonaws.services.dynamodbv2.model.ConditionalCheckFailedException;
import com.amazonaws.services.dynamodbv2.model.DeleteItemRequest;
import com.amazonaws.services.dynamodbv2.model.GetItemRequest;
import com.amazonaws.services.dynamodbv2.model.GetItemResult;
import com.amazonaws.services.dynamodbv2.model.GlobalSecondaryIndexDescription;
import com.amazonaws.services.dynamodbv2.model.KeySchemaElement;
import com.amazonaws.services.dynamodbv2.model.KeyType;
import com.amazonaws.services.dynamodbv2.model.KeysAndAttributes;
import com.amazonaws.services.dynamodbv2.model.LocalSecondaryIndexDescription;
import com.amazonaws.services.dynamodbv2.model.PutItemRequest;
import com.amazonaws.services.dynamodbv2.model.QueryRequest;
import com.amazonaws.services.dynamodbv2.model.ReturnValue;
import com.amazonaws.services.dynamodbv2.model.ScanRequest;
import com.amazonaws.services.dynamodbv2.model.TableDescription;
import com.amazonaws.services.dynamodbv2.model.UpdateItemRequest;
import com.amazonaws.services.dynamodbv2.model.UpdateItemResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* An implementation of the QueryModelDAO interface that works atop DynamoDB.
*
* @param <T> Persistent class
* @param <ID> ID type of the persistent class.
*/
public class DynamoDBQueryModelDAO<T, ID>
implements QueryModelDAO<T, ID> {
//-------------------------------------------------------------
// Constants
//-------------------------------------------------------------
private static final Logger logger = LoggerFactory.getLogger(DynamoDBQueryModelDAO.class);
//-------------------------------------------------------------
// Variables - Private
//-------------------------------------------------------------
private final Class<T> entityClass;
private final AmazonDynamoDB dynamoDB;
private final String tableName;
private final IdGenerator<ID> idGenerator;
private final boolean consistentRead;
private final String optimisticLockField;
private final boolean enableScans;
private final String hashKeyField;
private final String rangeKeyField;
private final String projectionExpression;
private final Map<String, String> projectionExpressionNames;
private final Map<String, Map<String, IndexData>> indexes;
private final Map<String, Map<String, IndexData>> baseIndexOnly;
private final Enhancer<T> persistableEnhancer;
private final Enhancer<? extends T> updateObjectEnhancer;
private final String uniqueIdConditionExpression;
//-------------------------------------------------------------
// Constructors
//-------------------------------------------------------------
protected DynamoDBQueryModelDAO(Class<T> entityClass, Map<String, Object> daoProperties) {
this(entityClass, daoProperties, null);
}
@SuppressWarnings({ "unchecked", "UnusedParameters" })
protected DynamoDBQueryModelDAO(Class<T> entityClass, Map<String, Object> daoProperties,
AccessControlContextProvider accessControlContextProvider) {
this.entityClass = entityClass;
this.dynamoDB = (AmazonDynamoDB) daoProperties.get("db");
this.tableName = daoProperties.containsKey("tableName") ? (String) daoProperties.get("tableName") : entityClass.getSimpleName();
this.idGenerator = (IdGenerator<ID>) daoProperties.get("idGenerator");
this.consistentRead = Boolean.parseBoolean((String) daoProperties.get("consistentRead")); // null okay - defaults to false
this.optimisticLockField = (String) daoProperties.get("optimisticLockField");
this.enableScans = Boolean.parseBoolean((String) daoProperties.get("enableScans")); // null okay - defaults to false
TableDescription tableDescription = dynamoDB.describeTable(tableName).getTable();
Pair<String, String> primaryKeyAttributeNames = getKeyAttributeNames(tableDescription.getKeySchema());
this.hashKeyField = primaryKeyAttributeNames.getFirst();
this.rangeKeyField = primaryKeyAttributeNames.getSecond();
ProjectionExpressionBuilder projectionExpressionBuilder;
if (Boolean.parseBoolean((String) daoProperties.get("projectionObject"))) { // null okay - defaults to false
projectionExpressionBuilder = new ProjectionExpressionBuilder(entityClass, hashKeyField, rangeKeyField, optimisticLockField);
this.projectionExpression = projectionExpressionBuilder.getExpression();
this.projectionExpressionNames = projectionExpressionBuilder.getExpressionAttributeNames();
} else {
projectionExpressionBuilder = null;
this.projectionExpression = null;
this.projectionExpressionNames = Collections.emptyMap();
}
List<String> keyFields = rangeKeyField == null ? Collections.singletonList(hashKeyField) : Arrays.asList(hashKeyField, rangeKeyField);
IndexData baseIndexData = new IndexData(null, keyFields, true);
this.baseIndexOnly = Collections.singletonMap(hashKeyField, Collections.singletonMap(rangeKeyField, baseIndexData));
this.indexes = processIndexes(tableDescription, projectionExpressionBuilder, baseIndexData);
this.persistableEnhancer = EnhancerHelper.getPersistableEnhancer(entityClass);
String updateObjectClassName = (String) daoProperties.get("updateObject");
if (updateObjectClassName == null) {
this.updateObjectEnhancer = EnhancerHelper.getUpdateObjectEnhancer(entityClass);
} else {
try {
Class updateObjectClass = Class.forName(updateObjectClassName);
if (!entityClass.isAssignableFrom(updateObjectClass)) {
throw new JeppettoException(String.format("Invalid UpdateObject type. %s does not subclass entity type %s",
updateObjectClassName, entityClass.getName()));
}
this.updateObjectEnhancer = (Enhancer<? extends T>) EnhancerHelper.getUpdateObjectEnhancer(updateObjectClass);
} catch (ClassNotFoundException e) {
throw new JeppettoException(e);
}
}
if (Boolean.parseBoolean((String) daoProperties.get("verifyUniqueIds"))) { // null okay - defaults to false
ConditionExpressionBuilder conditionExpressionBuilder
= new ConditionExpressionBuilder().with(hashKeyField, new DynamoDBConstraint(DynamoDBOperator.IsNull));
if (rangeKeyField != null) {
conditionExpressionBuilder.with(rangeKeyField, new DynamoDBConstraint(DynamoDBOperator.IsNull));
}
this.uniqueIdConditionExpression = conditionExpressionBuilder.getExpression(); // No attribute values needed
} else {
this.uniqueIdConditionExpression = null;
}
}
//-------------------------------------------------------------
// Implementation - GenericDAO
//-------------------------------------------------------------
@Override
public T findById(ID id)
throws NoSuchItemException, JeppettoException {
GetItemResult result;
try {
GetItemRequest getItemRequest = new GetItemRequest(tableName, getKeyFrom(id), consistentRead);
getItemRequest.setProjectionExpression(projectionExpression);
if (!projectionExpressionNames.isEmpty()) {
getItemRequest.setExpressionAttributeNames(projectionExpressionNames);
}
result = dynamoDB.getItem(getItemRequest);
} catch (AmazonClientException e) {
throw new JeppettoException(e);
}
if (result.getItem() == null) {
throw new NoSuchItemException(entityClass.getSimpleName(), id.toString());
}
T t = ConversionUtil.getObjectFromItem(result.getItem(), entityClass);
((DynamoDBPersistable) t).__markPersisted(dynamoDB.toString());
return t;
}
@Override
public Iterable<T> findByIds(ID... ids)
throws JeppettoException {
Collection<Map<String, AttributeValue>> keys = new ArrayList<Map<String, AttributeValue>>();
for (ID id : ids) {
keys.add(getKeyFrom(id));
}
KeysAndAttributes keysAndAttributes = new KeysAndAttributes().withKeys(keys);
keysAndAttributes.setConsistentRead(consistentRead);
keysAndAttributes.setProjectionExpression(projectionExpression);
if (!projectionExpressionNames.isEmpty()) {
keysAndAttributes.setExpressionAttributeNames(projectionExpressionNames);
}
BatchGetItemRequest batchGetItemRequest = new BatchGetItemRequest().withRequestItems(Collections.singletonMap(tableName, keysAndAttributes));
return new BatchGetIterable<T>(dynamoDB, persistableEnhancer, batchGetItemRequest, tableName);
}
@Override
public Iterable<T> findAll()
throws JeppettoException {
return findUsingQueryModel(new QueryModel());
}
@Override
public void save(T entity)
throws OptimisticLockException, JeppettoException {
DynamoDBPersistable dynamoDBPersistable = (DynamoDBPersistable) persistableEnhancer.enhance(entity);
if (!dynamoDBPersistable.__isPersisted(dynamoDB.toString())) {
if (optimisticLockField != null) {
dynamoDBPersistable.__put(optimisticLockField, new AttributeValue().withN("0"));
}
saveItem(dynamoDBPersistable);
} else {
ConditionExpressionBuilder conditionExpressionBuilder;
if (optimisticLockField != null) {
AttributeValue attributeValue = (AttributeValue) dynamoDBPersistable.__get(optimisticLockField);
int optimisticLockVersion;
if (attributeValue != null) {
optimisticLockVersion = Integer.parseInt(attributeValue.getN());
conditionExpressionBuilder = new ConditionExpressionBuilder();
conditionExpressionBuilder.with(optimisticLockField, new DynamoDBConstraint(DynamoDBOperator.Equal, optimisticLockVersion));
} else {
optimisticLockVersion = -1;
conditionExpressionBuilder = null;
}
dynamoDBPersistable.__put(optimisticLockField, new AttributeValue().withN(Integer.toString(optimisticLockVersion + 1)));
} else {
conditionExpressionBuilder = null;
}
try {
UpdateExpressionBuilder updateExpressionBuilder = new UpdateExpressionBuilder(dynamoDBPersistable);
updateItem(getKeyFrom(dynamoDBPersistable), updateExpressionBuilder, conditionExpressionBuilder, ResultFromUpdate.ReturnNone);
} catch (JeppettoException e) {
if (optimisticLockField != null && e.getCause() instanceof ConditionalCheckFailedException) {
throw new OptimisticLockException(e.getCause());
} else {
throw e;
}
}
}
dynamoDBPersistable.__markPersisted(dynamoDB.toString());
}
@Override
public void delete(T entity)
throws JeppettoException {
if (entity == null) {
throw new JeppettoException("entity is null; nothing to delete.");
}
deleteItem(getKeyFrom((DynamoDBPersistable) persistableEnhancer.enhance(entity)));
}
@Override
public void deleteById(ID id)
throws JeppettoException {
if (id == null) {
throw new JeppettoException("id is null; unable to delete entity.");
}
deleteItem(getKeyFrom(id));
}
@Override
public void deleteByIds(ID... ids)
throws FailedBatchException, JeppettoException {
List<ID> succeeded = new ArrayList<ID>();
Map<ID, Exception> failed = new LinkedHashMap<ID, Exception>();
for (ID id : ids) {
try {
deleteItem(getKeyFrom(id));
succeeded.add(id);
} catch (Exception e) {
//noinspection ThrowableResultOfMethodCallIgnored
failed.put(id, e);
}
}
if (failed.size() > 0) {
throw new FailedBatchException("Unable to delete all items", succeeded, failed);
}
}
@Override
public <U extends T> U getUpdateObject() {
//noinspection unchecked
return (U) updateObjectEnhancer.newInstance();
}
@Override
public <U extends T> T updateById(U updateObject, ID id)
throws JeppettoException {
return updateItem(getKeyFrom(id), new UpdateExpressionBuilder((UpdateObject) updateObject), null, getResultFromUpdate(updateObject));
}
@Override
public <U extends T> Iterable<T> updateByIds(U updateObject, ID... ids)
throws FailedBatchException, JeppettoException {
List<?> succeeded;
Map<ID, Exception> failed = new LinkedHashMap<ID, Exception>();
ResultFromUpdate resultFromUpdate = getResultFromUpdate(updateObject);
if (resultFromUpdate == ResultFromUpdate.ReturnNone) {
succeeded = new ArrayList<ID>();
} else {
succeeded = new ArrayList<T>();
}
UpdateExpressionBuilder updateExpressionBuilder = new UpdateExpressionBuilder((UpdateObject) updateObject);
for (ID id : ids) {
try {
T t = updateItem(getKeyFrom(id), updateExpressionBuilder, null, resultFromUpdate);
if (resultFromUpdate == ResultFromUpdate.ReturnNone) {
//noinspection unchecked
((List<ID>) succeeded).add(id);
} else {
//noinspection unchecked
((List<T>) succeeded).add(t);
}
} catch (Exception e) {
//noinspection ThrowableResultOfMethodCallIgnored
failed.put(id, e);
}
}
if (failed.size() > 0) {
throw new FailedBatchException("Unable to update all items", succeeded, failed);
}
//noinspection unchecked
return resultFromUpdate == ResultFromUpdate.ReturnNone ? null : (Iterable<T>) succeeded;
}
@Override
public void flush()
throws JeppettoException {
// Flush not required.
}
//-------------------------------------------------------------
// Implementation - QueryModelDAO
//-------------------------------------------------------------
@Override
public T findUniqueUsingQueryModel(QueryModel queryModel)
throws NoSuchItemException, TooManyItemsException, JeppettoException {
DynamoDBIterable<T> dynamoDBIterable = (DynamoDBIterable<T>) findUsingQueryModel(queryModel);
dynamoDBIterable.setLimit(1);
Iterator<T> results = dynamoDBIterable.iterator();
if (!results.hasNext()) {
throw new NoSuchItemException();
}
T result = results.next();
if (dynamoDBIterable.hasResultsPastLimit()) {
throw new TooManyItemsException();
}
return result;
}
@Override
public Iterable<T> findUsingQueryModel(QueryModel queryModel)
throws JeppettoException {
ConditionExpressionBuilder conditionExpressionBuilder = new ConditionExpressionBuilder(queryModel, indexes);
if (conditionExpressionBuilder.hasHashKeyCondition()) {
return queryItems(queryModel, conditionExpressionBuilder);
} else if (enableScans) {
logger.info("Condition does not specify a hash key -- using 'scan' to search.");
conditionExpressionBuilder.convertRangeKeyConditionToExpression();
return scanItems(queryModel, conditionExpressionBuilder);
} else {
throw new JeppettoException("Find cannot be satisfied without a scan and scans have not been enabled."
+ " Configure this DAO with 'enableScans' = true to allow this.");
}
}
@Override
public Object projectUsingQueryModel(QueryModel queryModel)
throws JeppettoException {
// TODO: handle count case. not sure about other projections...
throw new UnsupportedOperationException("Projections on DynamoDB are not currently supported.");
}
@Override
public void deleteUsingQueryModel(QueryModel queryModel)
throws JeppettoException {
Iterable<T> matches = findUsingQueryModel(queryModel);
// Would ideally catch individual exceptions and throw a FailedBatchException. Unfortunately, we don't have an easy
// way to convert the key from a match to an ID object, which is what the exception's Map contains. Maybe DynamoDB will
// add support for a query-based delete.
for (T match : matches) {
delete(match);
}
}
@Override
public <U extends T> T updateUniqueUsingQueryModel(U updateObject, QueryModel queryModel)
throws JeppettoException {
UpdateExpressionBuilder updateExpressionBuilder = new UpdateExpressionBuilder((UpdateObject) updateObject);
// For referencing an object, we can only identify an item by its actual range key, not one of the index fields.
ConditionExpressionBuilder conditionExpressionBuilder = new ConditionExpressionBuilder(queryModel, baseIndexOnly);
ResultFromUpdate resultFromUpdate = getResultFromUpdate(updateObject);
Map<String, AttributeValue> key;
try {
key = conditionExpressionBuilder.getKey();
} catch (NullPointerException e) {
throw new JeppettoException("DynamoDB only supports updates where the condition uniquely identifies the item by its key.", e);
}
return updateItem(key, updateExpressionBuilder, conditionExpressionBuilder, resultFromUpdate);
}
@Override
public <U extends T> Iterable<T> updateUsingQueryModel(U updateObject, QueryModel queryModel)
throws JeppettoException {
// DynamoDB only supports updating a single item at a time.
T t = updateUniqueUsingQueryModel(updateObject, queryModel);
if (t == null) {
return null;
}
return Collections.singletonList(t);
}
@Override
public Condition buildCondition(String conditionField, ConditionType conditionType, Iterator argsIterator) {
return new Condition(conditionField, DynamoDBOperator.valueOf(conditionType.name()).buildConstraint(argsIterator));
}
@Override
public Projection buildProjection(String projectionField, ProjectionType projectionType, Iterator argsIterator) {
throw new UnsupportedOperationException("Projections on DynamoDB are not currently supported.");
}
//-------------------------------------------------------------
// Methods - Private
//-------------------------------------------------------------
private Map<String, Map<String, IndexData>> processIndexes(TableDescription tableDescription,
ProjectionExpressionBuilder projectionExpressionBuilder,
IndexData baseIndexData) {
// Collect information about the local secondary indexes. These will be included with the global indexes below.
Map<String, IndexData> localIndexes;
List<LocalSecondaryIndexDescription> localSecondaryIndexes = tableDescription.getLocalSecondaryIndexes();
if (localSecondaryIndexes != null) {
localIndexes = new HashMap<String, IndexData>(localSecondaryIndexes.size() + 2);
// We include these as local indexes to make findUsingQueryModel() code below simpler
localIndexes.put(rangeKeyField, baseIndexData);
localIndexes.put(null, baseIndexData);
for (LocalSecondaryIndexDescription description : localSecondaryIndexes) {
String indexField = getKeyAttributeNames(description.getKeySchema()).getSecond();
boolean projectsOverEntity = description.getProjection().getProjectionType().equals("ALL")
|| projectionExpressionBuilder != null
&& projectionExpressionBuilder.isCoveredBy(description.getProjection());
List<String> keyFields = new ArrayList<String>(baseIndexData.keyFields);
keyFields.add(indexField);
localIndexes.put(indexField, new IndexData(description.getIndexName(), keyFields, projectsOverEntity));
}
} else if (rangeKeyField != null) {
localIndexes = new HashMap<String, IndexData>(2);
localIndexes.put(rangeKeyField, baseIndexData);
localIndexes.put(null, baseIndexData);
} else {
localIndexes = Collections.singletonMap(null, baseIndexData);
}
// Process the global secondary indexes. When done, add the local index information.
List<GlobalSecondaryIndexDescription> globalSecondaryIndexes = tableDescription.getGlobalSecondaryIndexes();
if (globalSecondaryIndexes != null) {
Map<String, Map<String, IndexData>> indexes = new HashMap<String, Map<String, IndexData>>(globalSecondaryIndexes.size() + 1);
for (GlobalSecondaryIndexDescription description : globalSecondaryIndexes) {
Pair<String, String> indexFields = getKeyAttributeNames(description.getKeySchema());
boolean projectsOverEntity = description.getProjection().getProjectionType().equals("ALL")
|| projectionExpressionBuilder != null
&& projectionExpressionBuilder.isCoveredBy(description.getProjection());
List<String> keyFields = new ArrayList<String>();
keyFields.add(indexFields.getFirst());
if (indexFields.getSecond() != null) {
keyFields.add(indexFields.getSecond());
}
keyFields.add(hashKeyField);
if (rangeKeyField != null) {
keyFields.add(rangeKeyField);
}
IndexData indexData = new IndexData(description.getIndexName(), keyFields, projectsOverEntity);
if (!indexes.containsKey(indexFields.getFirst())) {
indexes.put(indexFields.getFirst(), new HashMap<String, IndexData>());
}
indexes.get(indexFields.getFirst()).put(indexFields.getSecond(), indexData);
// In case a query doesn't specify a range key, we still want to select an index for this hash key.
// If one has already been selected, pick one that projects over this entity to avoid extra DB reads.
IndexData noRangeKeyIndexData = indexes.get(indexFields.getFirst()).get(null);
if (noRangeKeyIndexData == null || !noRangeKeyIndexData.projectsOverEntity) {
indexes.get(indexFields.getFirst()).put(null, indexData);
}
}
indexes.put(hashKeyField, localIndexes);
return indexes;
} else {
return Collections.singletonMap(hashKeyField, localIndexes);
}
}
private void saveItem(DynamoDBPersistable dynamoDBPersistable) {
generateIdIfNeeded(dynamoDBPersistable);
try {
PutItemRequest putItemRequest = new PutItemRequest().withTableName(tableName)
.withItem(ConversionUtil.getItemFromObject(dynamoDBPersistable))
.withConditionExpression(uniqueIdConditionExpression);
dynamoDB.putItem(putItemRequest);
} catch (Exception e) {
throw new JeppettoException(e);
}
}
private T updateItem(Map<String, AttributeValue> key, UpdateExpressionBuilder updateExpressionBuilder,
ConditionExpressionBuilder conditionExpressionBuilder, ResultFromUpdate resultFromUpdate) {
try {
UpdateItemRequest updateItemRequest = new UpdateItemRequest().withTableName(tableName)
.withKey(key)
.withUpdateExpression(updateExpressionBuilder.getExpression());
Map<String, AttributeValue> expressionAttributeValues;
Map<String, String> expressionAttributeNames;
if (conditionExpressionBuilder == null) {
expressionAttributeValues = updateExpressionBuilder.getExpressionAttributeValues();
expressionAttributeNames = updateExpressionBuilder.getExpressionAttributeNames();
} else {
expressionAttributeValues = new LinkedHashMap<String, AttributeValue>();
expressionAttributeNames = new LinkedHashMap<String, String>();
expressionAttributeValues.putAll(updateExpressionBuilder.getExpressionAttributeValues());
expressionAttributeNames.putAll(updateExpressionBuilder.getExpressionAttributeNames());
expressionAttributeValues.putAll(conditionExpressionBuilder.getExpressionAttributeValues());
expressionAttributeNames.putAll(conditionExpressionBuilder.getExpressionAttributeNames());
updateItemRequest.setConditionExpression(conditionExpressionBuilder.getExpression());
}
if (!expressionAttributeValues.isEmpty()) {
updateItemRequest.setExpressionAttributeValues(expressionAttributeValues);
}
if (!expressionAttributeNames.isEmpty()) {
updateItemRequest.setExpressionAttributeNames(expressionAttributeNames);
}
if (resultFromUpdate != ResultFromUpdate.ReturnNone) {
updateItemRequest.setReturnValues(resultFromUpdate == ResultFromUpdate.ReturnPreUpdate ? ReturnValue.ALL_OLD
: ReturnValue.ALL_NEW);
UpdateItemResult result = dynamoDB.updateItem(updateItemRequest);
T t = ConversionUtil.getObjectFromItem(result.getAttributes(), entityClass);
((DynamoDBPersistable) t).__markPersisted(dynamoDB.toString());
return t;
} else {
dynamoDB.updateItem(updateItemRequest);
return null;
}
} catch (Exception e) {
throw new JeppettoException(e);
}
}
private void deleteItem(Map<String, AttributeValue> key) {
try {
dynamoDB.deleteItem(new DeleteItemRequest(tableName, key));
} catch (Exception e) {
throw new JeppettoException(e);
}
}
private Iterable<T> queryItems(QueryModel queryModel, ConditionExpressionBuilder conditionExpressionBuilder) {
QueryRequest queryRequest = new QueryRequest(tableName);
queryRequest.setKeyConditions(conditionExpressionBuilder.getKeyConditions());
queryRequest.setConsistentRead(consistentRead);
if (queryModel.getFirstResult() > 0) {
logger.warn("DynamoDB does not support skipping results. Call setPosition() on DynamoDBIterable instead.");
}
if (queryModel.getMaxResults() > 0) {
queryRequest.setLimit(queryModel.getMaxResults());
}
List<String> keyFields = applyIndexAndGetKeyFields(conditionExpressionBuilder, queryRequest, queryModel.getSorts());
applyExpressions(conditionExpressionBuilder, queryRequest);
return new QueryIterable<T>(dynamoDB, persistableEnhancer, queryRequest, keyFields.get(0), keyFields);
}
private List<String> applyIndexAndGetKeyFields(ConditionExpressionBuilder conditionExpressionBuilder,
QueryRequest queryRequest, List<Sort> sorts) {
String hashKey = conditionExpressionBuilder.getHashKey();
String rangeKey = conditionExpressionBuilder.getRangeKey();
IndexData indexData;
if (sorts == null || sorts.isEmpty()) {
indexData = indexes.get(hashKey).get(rangeKey);
} else if (sorts.size() == 1) {
Sort sort = sorts.get(0);
String sortKey = sort.getField();
// DynamoDB can only sort on the effective range key. If a range key is specified, ensure the range key and
// sort key are the same.
if (rangeKey != null && !rangeKey.equals(sortKey)) {
throw new JeppettoException("DynamoDB can only sort on the effective range key. Unable to sort on: " + sortKey);
}
queryRequest.setScanIndexForward(sort.getSortDirection() == SortDirection.Ascending);
// Index is based off the sort key
indexData = indexes.get(hashKey).get(sortKey);
} else {
throw new JeppettoException("DynamoDB only supports one sort value.");
}
if (indexData.indexName != null && !indexData.projectsOverEntity) {
logger.warn("Query using index {} incurs additional costs to fully fetch a {} type. Use a projected object"
+ " DAO to avoid this overhead.", indexData.indexName, entityClass.getSimpleName());
}
queryRequest.setIndexName(indexData.indexName);
return indexData.keyFields;
}
private void applyExpressions(ConditionExpressionBuilder conditionExpressionBuilder, QueryRequest queryRequest) {
Map<String, String> expressionAttributeNames;
queryRequest.setProjectionExpression(projectionExpression);
if (conditionExpressionBuilder.hasExpression()) {
queryRequest.setFilterExpression(conditionExpressionBuilder.getExpression());
if (!conditionExpressionBuilder.getExpressionAttributeValues().isEmpty()) {
queryRequest.setExpressionAttributeValues(conditionExpressionBuilder.getExpressionAttributeValues());
}
if (projectionExpressionNames.isEmpty()) {
expressionAttributeNames = conditionExpressionBuilder.getExpressionAttributeNames();
} else if (conditionExpressionBuilder.getExpressionAttributeNames().isEmpty()) {
expressionAttributeNames = projectionExpressionNames;
} else {
expressionAttributeNames = new LinkedHashMap<String, String>();
expressionAttributeNames.putAll(conditionExpressionBuilder.getExpressionAttributeNames());
expressionAttributeNames.putAll(projectionExpressionNames);
}
} else {
expressionAttributeNames = projectionExpressionNames;
}
if (!expressionAttributeNames.isEmpty()) {
queryRequest.setExpressionAttributeNames(expressionAttributeNames);
}
}
private Iterable<T> scanItems(QueryModel queryModel, ConditionExpressionBuilder conditionExpressionBuilder) {
ScanRequest scanRequest = new ScanRequest(tableName);
if (queryModel.getFirstResult() > 0) {
logger.warn("DynamoDB does not support skipping results. Call setPosition() on DynamoDBIterable instead.");
}
if (queryModel.getMaxResults() > 0) {
scanRequest.setLimit(queryModel.getMaxResults());
}
if (queryModel.getSorts() != null) {
logger.warn("Not able to sort when performing a 'scan' operation. Ignoring... ");
}
Map<String, String> expressionAttributeNames;
scanRequest.setProjectionExpression(projectionExpression);
if (conditionExpressionBuilder.hasExpression()) {
scanRequest.setFilterExpression(conditionExpressionBuilder.getExpression());
if (!conditionExpressionBuilder.getExpressionAttributeValues().isEmpty()) {
scanRequest.setExpressionAttributeValues(conditionExpressionBuilder.getExpressionAttributeValues());
}
if (projectionExpressionNames.isEmpty()) {
expressionAttributeNames = conditionExpressionBuilder.getExpressionAttributeNames();
} else if (conditionExpressionBuilder.getExpressionAttributeNames().isEmpty()) {
expressionAttributeNames = projectionExpressionNames;
} else {
expressionAttributeNames = new LinkedHashMap<String, String>();
expressionAttributeNames.putAll(conditionExpressionBuilder.getExpressionAttributeNames());
expressionAttributeNames.putAll(projectionExpressionNames);
}
} else {
expressionAttributeNames = projectionExpressionNames;
}
if (!expressionAttributeNames.isEmpty()) {
scanRequest.setExpressionAttributeNames(expressionAttributeNames);
}
return new ScanIterable<T>(dynamoDB, persistableEnhancer, scanRequest,
rangeKeyField == null ? Collections.singleton(hashKeyField)
: Arrays.asList(hashKeyField, rangeKeyField));
}
private <U extends T> ResultFromUpdate getResultFromUpdate(U updateObject) {
if (UpdateBehaviorDescriptor.class.isAssignableFrom(updateObject.getClass())) {
ResultFromUpdate resultFromUpdate = ((UpdateBehaviorDescriptor) updateObject).getResultFromUpdate();
return resultFromUpdate != null ? resultFromUpdate : ResultFromUpdate.ReturnNone;
} else {
return ResultFromUpdate.ReturnNone;
}
}
private void generateIdIfNeeded(DynamoDBPersistable dynamoDBPersistable) {
if (dynamoDBPersistable.__get(hashKeyField) != null
/* && rangeKeyField != null && dynamoDBPersistable.__get(rangeKeyField) != null */) {
return;
}
if (idGenerator == null) {
throw new JeppettoException("No id provided, and no id generator available.");
}
// TODO: handle case when part of the key is there (e.g. code generates range key, but wants to generate hash key)
// Can't blindly use getKeyFrom since a single generated value may be for the range key...
dynamoDBPersistable.__putAll(getKeyFrom(idGenerator.generateId()));
}
private AttributeValue getAttributeValue(Object value) {
if (Number.class.isAssignableFrom(value.getClass())) {
return new AttributeValue().withN(value.toString());
} else {
return new AttributeValue(value.toString());
}
}
private Pair<String, String> getKeyAttributeNames(List<KeySchemaElement> keySchema) {
Pair<String, String> keyAttributes = new Pair<String, String>();
for (KeySchemaElement keySchemaElement : keySchema) {
if (keySchemaElement.getKeyType().equals(KeyType.HASH.name())) {
keyAttributes.setFirst(keySchemaElement.getAttributeName());
} else {
keyAttributes.setSecond(keySchemaElement.getAttributeName());
}
}
return keyAttributes;
}
private Map<String, AttributeValue> getKeyFrom(ID id) {
Map<String, AttributeValue> key;
if (Pair.class.isAssignableFrom(id.getClass())) {
key = new HashMap<String, AttributeValue>(2);
key.put(hashKeyField, getAttributeValue(((Pair) id).getFirst()));
key.put(rangeKeyField, getAttributeValue(((Pair) id).getSecond()));
} else {
key = Collections.singletonMap(hashKeyField, getAttributeValue(id));
}
return key;
}
private Map<String, AttributeValue> getKeyFrom(DynamoDBPersistable dynamoDBPersistable) {
Map<String, AttributeValue> key;
if (rangeKeyField != null) {
key = new HashMap<String, AttributeValue>(2);
key.put(hashKeyField, ConversionUtil.toAttributeValue(dynamoDBPersistable.__get(hashKeyField)));
key.put(rangeKeyField, ConversionUtil.toAttributeValue(dynamoDBPersistable.__get(rangeKeyField)));
} else {
key = Collections.singletonMap(hashKeyField, ConversionUtil.toAttributeValue(dynamoDBPersistable.__get(
hashKeyField)));
}
return key;
}
//-------------------------------------------------------------
// Inner Classes
//-------------------------------------------------------------
public static class IndexData {
public String indexName;
public List<String> keyFields;
public boolean projectsOverEntity;
private IndexData(String indexName, List<String> keyFields, boolean projectsOverEntity) {
this.indexName = indexName;
this.keyFields = keyFields;
this.projectsOverEntity = projectsOverEntity;
}
}
}