package org.springframework.data.simpledb.core; import java.io.Serializable; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.data.simpledb.attributeutil.SimpleDBAttributeConverter; import org.springframework.data.simpledb.attributeutil.SimpleDbAttributeValueSplitter; import org.springframework.data.simpledb.core.entity.EntityWrapper; import org.springframework.data.simpledb.core.entity.json.JsonMarshaller; import org.springframework.data.simpledb.exception.InvalidSimpleDBQueryException; import org.springframework.data.simpledb.parser.SimpleDBParser; import org.springframework.data.simpledb.query.QueryUtils; import org.springframework.data.simpledb.reflection.FieldType; import org.springframework.data.simpledb.reflection.FieldTypeIdentifier; import org.springframework.data.simpledb.reflection.MetadataParser; import org.springframework.data.simpledb.reflection.ReflectionUtils; import org.springframework.data.simpledb.repository.support.entityinformation.SimpleDbEntityInformation; import org.springframework.data.simpledb.repository.support.entityinformation.SimpleDbEntityInformationSupport; import org.springframework.util.Assert; import com.amazonaws.services.simpledb.model.Attribute; import com.amazonaws.services.simpledb.model.BatchDeleteAttributesRequest; import com.amazonaws.services.simpledb.model.DeletableItem; import com.amazonaws.services.simpledb.model.DeleteAttributesRequest; import com.amazonaws.services.simpledb.model.Item; import com.amazonaws.services.simpledb.model.PutAttributesRequest; import com.amazonaws.services.simpledb.model.SelectRequest; import com.amazonaws.services.simpledb.model.SelectResult; /** * Primary implementation of {@link SimpleDbOperations} */ public class SimpleDbTemplate extends AbstractSimpleDbTemplate { private static final int MAX_BATCH_SIZE = 25; private static final Logger LOGGER = LoggerFactory.getLogger(SimpleDbTemplate.class); public SimpleDbTemplate(SimpleDb simpleDb) { super(simpleDb); } @Override public <T> T createOrUpdateImpl(T domainItem, EntityWrapper<T, ?> entity) { Assert.notNull(entity.getDomain(), "Domain name should not be null"); logOperation("Create or update", entity); for (final Field field : ReflectionUtils.getFirstLevelOfReferenceAttributes(domainItem.getClass())) { final Object referenceEntity = ReflectionUtils.callGetter(domainItem, field.getName()); /* recursive call */ if (referenceEntity != null) { createOrUpdate(referenceEntity); } } if (entity.getItemName() != null) { delete(entity.getDomain(), entity.getItemName()); } entity.generateIdIfNotSet(); Map<String, List<String>> rawAttributes = entity.toMultiValueAttributes(); List<PutAttributesRequest> putAttributesRequests = SimpleDbRequestBuilder.createPutAttributesRequests( entity.getDomain(), entity.getItemName(), rawAttributes); for (PutAttributesRequest request : putAttributesRequests) { getDB().putAttributes(request); } return entity.getItem(); } @Override public void deleteAttributesImpl(String domainName, String itemName) { LOGGER.debug("Delete Domain\"{}\" ItemName \"{}\"", domainName, itemName); Assert.notNull(domainName, "Domain name should not be null"); Assert.notNull(itemName, "Item name should not be null"); getDB().deleteAttributes(new DeleteAttributesRequest(domainName, itemName)); } @Override public <T> void deleteImpl(T domainItem, SimpleDbEntityInformation<T, ?> entityInformation, EntityWrapper<T, ?> entity) { for (final Field field : ReflectionUtils.getFirstLevelOfReferenceAttributes(domainItem.getClass())) { final Object referenceEntity = ReflectionUtils.callGetter(domainItem, field.getName()); /* recursive call */ if (referenceEntity != null) { delete(referenceEntity); } } delete(entity.getDomain(), entity.getItemName()); } @Override public <T, ID> void delete(Class<T> entityClass, Iterable<? extends ID> ids) { if (ids.iterator().hasNext()) { String domainName = getDomainName(entityClass); List<DeletableItem> deleteList = new ArrayList<DeletableItem>(); for (ID id : ids) { deleteList.add(new DeletableItem().withName((String) id)); } // max allowed batch size is 25 List<DeletableItem> batch = new ArrayList<DeletableItem>(MAX_BATCH_SIZE); for (int i = 0; i < deleteList.size(); i += MAX_BATCH_SIZE) { int batchIndex = ((i + MAX_BATCH_SIZE) < deleteList.size()) ? (i + MAX_BATCH_SIZE) : deleteList.size(); for (int j = i; j < batchIndex; j++) { batch.add(deleteList.get(j)); } LOGGER.debug(String.format("Batch size: %d", batch.size())); getDB().batchDeleteAttributes(new BatchDeleteAttributesRequest( domainName, batch)); batch.clear(); } } } @Override public SelectResult invokeFindImpl(boolean consistentRead, String escapedQuery) { LOGGER.debug("Query: {}", escapedQuery); return getDB().select(new SelectRequest(escapedQuery, consistentRead)); } @Override public <T, ID extends Serializable> T readImpl(ID id, Class<T> entityClass, boolean consistentRead, SimpleDbEntityInformation<T, ?> entityInformation) { LOGGER.debug("Read ItemName \"{}\"", id); List<ID> ids = new ArrayList<ID>(); { ids.add(id); } List<T> results = find(entityClass, new QueryBuilder(entityInformation).withIds(ids).toString(), consistentRead); return results.size() == 1 ? results.get(0) : null; } @Override public <T> long countImpl(String query, boolean consistentRead, SimpleDbEntityInformation<T, ?> entityInformation) { final String countQuery = new QueryBuilder(query, true).toString(); return invokeCountImpl(consistentRead, entityInformation, countQuery); } @Override public <T> long countImpl(boolean consistentRead, SimpleDbEntityInformation<T, ?> entityInformation) { final String countQuery = new QueryBuilder(entityInformation, true).toString(); return invokeCountImpl(consistentRead, entityInformation, countQuery); } private <T> long invokeCountImpl(boolean consistentRead, SimpleDbEntityInformation<T, ?> entityInformation, final String countQuery) { LOGGER.debug("Count items for query " + countQuery); validateSelectQuery(countQuery); final String escapedQuery = getEscapedQuery(countQuery, entityInformation); final SelectResult selectResult = invokeFindImpl(consistentRead, escapedQuery); for (Item item : selectResult.getItems()) { if (item.getName().equals("Domain")) { for (Attribute attribute : item.getAttributes()) { if (attribute.getName().equals("Count")) { return Long.parseLong(attribute.getValue()); } } } } return 0; } @Override public <T> List<T> findAllQueryImpl(Class<T> entityClass, SimpleDbEntityInformation<T, ?> entityInformation) { final String findAllQuery = new QueryBuilder(entityInformation).toString(); return find(entityClass, findAllQuery); } @Override public <T> Page<T> executePagedQueryImpl(Class<T> entityClass, String query, Pageable pageable, boolean consistentRead, SimpleDbEntityInformation<T, ?> entityInformation) { Assert.notNull(pageable); Assert.isTrue(pageable.getPageNumber() >= 0); Assert.isTrue(pageable.getPageSize() > 0); final String escapedQuery = getEscapedQuery(query, entityInformation); List<T> resultsList; String queryWithPageSizeLimit = new QueryBuilder(escapedQuery).with(pageable).toString(); if (pageable.getPageNumber() > 0) { String pageOffsetToken = getPageOffsetToken(pageable, entityInformation, escapedQuery, consistentRead); if (pageOffsetToken != null && !pageOffsetToken.isEmpty()) { resultsList = find(entityInformation, queryWithPageSizeLimit, pageOffsetToken, consistentRead); } else { resultsList = Collections.emptyList(); } } else { resultsList = find(entityClass, queryWithPageSizeLimit, consistentRead); } final String countQuery = new QueryBuilder(escapedQuery, true).toString(); Long totalCount = count(countQuery, entityClass, consistentRead); return new PageImpl<T>(resultsList, pageable, totalCount); } @Override public <T> List<T> recursiveFindImpl(Class<T> entityClass, String query, boolean consistentRead, SimpleDbEntityInformation<T, ?> entityInformation) { LOGGER.debug("Find All Domain \"{}\" isConsistent=\"{}\"", entityInformation.getDomain(), consistentRead); validateSelectQuery(query); final String escapedQuery = getEscapedQuery(query, entityInformation); List<T> result = new ArrayList<T>(); List<String> referenceFieldsNames = ReflectionUtils.getReferencedAttributeNames(entityClass); final DomainItemBuilder<T> domainItemBuilder = new DomainItemBuilder<T>(); final SelectResult selectResult = invokeFindImpl(consistentRead, escapedQuery); if (referenceFieldsNames.isEmpty()) { return domainItemBuilder.populateDomainItems(entityInformation, selectResult); } for (Item item : selectResult.getItems()) { T populatedItem = domainItemBuilder.populateDomainItem(entityInformation, item); result.add(populatedItem); for (Attribute attribute : item.getAttributes()) { if (!referenceFieldsNames.contains(attribute.getName())) { continue; } Class<?> referenceEntityClazz = ReflectionUtils.getFieldClass(entityClass, attribute.getName()); Object referenceEntity = read(attribute.getValue(), referenceEntityClazz); ReflectionUtils.callSetter(populatedItem, attribute.getName(), referenceEntity); } } return result; } @Override public <T> List<T> findImpl(SimpleDbEntityInformation<T, ?> entityInformation, String query, String nextToken, boolean consistentRead) { LOGGER.debug("Find All Domain \"{}\" isConsistent=\"{}\", with token!", entityInformation.getDomain(), consistentRead); final DomainItemBuilder<T> domainItemBuilder = new DomainItemBuilder<T>(); validateSelectQuery(query); final String escapedQuery = getEscapedQuery(query, entityInformation); SelectRequest selectRequest = new SelectRequest(escapedQuery, consistentRead); selectRequest.setNextToken(nextToken); final SelectResult selectResult = getDB().select(selectRequest); return domainItemBuilder.populateDomainItems(entityInformation, selectResult); } @SuppressWarnings("unchecked") @Override protected <T, ID> void updateImpl(ID id, Class<T> entityClass, Map<String, ? extends Object> propertyMap) { // From the propertyMap, retrieve the Field which will be updated, // from the Field, serialize the corresponding Object value as per // FieldWrapper#serialize semantics, plug into the scheme to convert // to item and send a put request. String domainName = getDomainName(entityClass); Map<String, String> serializedValues = new LinkedHashMap<String, String>(); for (Map.Entry<String, ?> entry : propertyMap.entrySet()) { String propertyPath = entry.getKey(); Object propertyValue = entry.getValue(); if (propertyValue == null) { continue; } String serializedPropertyValue = null; Field propertyField = ReflectionUtils.getPropertyField(entityClass, propertyPath); if (FieldTypeIdentifier.isOfType(propertyField, FieldType.PRIMITIVE, FieldType.CORE_TYPE)) { serializedPropertyValue = SimpleDBAttributeConverter.encode(propertyValue); } else if (FieldTypeIdentifier.isOfType(propertyField, FieldType.NESTED_ENTITY)) { SimpleDbEntityInformation<T, Serializable> entityMetadata = (SimpleDbEntityInformation<T, Serializable>) SimpleDbEntityInformationSupport.getMetadata(propertyValue.getClass(), domainName); EntityWrapper<T, Serializable> entity = new EntityWrapper<T, Serializable>(entityMetadata, (T) propertyValue, true); Map<String, String> nestedAttributes = entity.serialize(); // add to serializedValues after prefixing propertyPath for (Map.Entry<String, String> e : nestedAttributes.entrySet()) { String key = String.format("%s.%s", propertyPath, e.getKey()); serializedValues.put(key, e.getValue()); } } else { serializedPropertyValue = JsonMarshaller.getInstance().marshall(propertyValue); } if (serializedPropertyValue != null) { serializedValues.put(propertyPath, serializedPropertyValue); } } Map<String, List<String>> rawAttributes = SimpleDbAttributeValueSplitter.splitAttributeValuesWithExceedingLengths(serializedValues); List<PutAttributesRequest> putAttributesRequests = SimpleDbRequestBuilder.createPutAttributesRequests( domainName, (String) id, rawAttributes); for (PutAttributesRequest request : putAttributesRequests) { getDB().putAttributes(request); } } private <T> String getEscapedQuery(String query, SimpleDbEntityInformation<T, ?> entityInformation) { return QueryUtils.escapeQueryAttributes(query, MetadataParser.getIdField(entityInformation.getJavaType()) .getName()); } /* * Validate a custom query before sending the request to the DB. */ private void validateSelectQuery(final String selectQuery) { final SimpleDBParser parser = new SimpleDBParser(selectQuery); try { parser.selectQuery(); } catch (Exception e) { throw new InvalidSimpleDBQueryException("The following query is an invalid SimpleDB query: " + selectQuery, e); } } private String getNextToken(String query, boolean consistentRead) { LOGGER.debug("Get next token for query: " + query); Assert.isTrue(query.contains("limit"), "Only queries with limit have a next token!"); final SelectResult selectResult = getDB().select(new SelectRequest(query, consistentRead)); return selectResult.getNextToken(); } private <T> String getPageOffsetToken(final Pageable pageable, SimpleDbEntityInformation<T, ?> entityInformation, String query, boolean consistentRead) { int endOfPreviousPageLimit = pageable.getPageNumber() * pageable.getPageSize(); final String escapedQuery = getEscapedQuery(query, entityInformation); final String countQuery = new QueryBuilder(escapedQuery, true).withLimit(endOfPreviousPageLimit).toString(); return getNextToken(countQuery, consistentRead); } private void logOperation(String operation, EntityWrapper<?, ?> entity) { LOGGER.debug(operation + " \"{}\" ItemName \"{}\"", entity.getDomain(), entity.getItemName()); } }