View Javadoc

1   /**
2    * Copyright © 2018 spring-data-dynamodb (https://github.com/derjust/spring-data-dynamodb)
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.socialsignin.spring.data.dynamodb.repository.query;
17  
18  import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperFieldModel;
19  import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperTableModel;
20  import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMarshaller;
21  import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBQueryExpression;
22  import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTypeConverter;
23  import com.amazonaws.services.dynamodbv2.model.AttributeValue;
24  import com.amazonaws.services.dynamodbv2.model.ComparisonOperator;
25  import com.amazonaws.services.dynamodbv2.model.Condition;
26  import com.amazonaws.services.dynamodbv2.model.QueryRequest;
27  import com.amazonaws.services.dynamodbv2.model.Select;
28  import org.socialsignin.spring.data.dynamodb.core.DynamoDBOperations;
29  import org.socialsignin.spring.data.dynamodb.marshaller.Date2IsoDynamoDBMarshaller;
30  import org.socialsignin.spring.data.dynamodb.marshaller.Instant2IsoDynamoDBMarshaller;
31  import org.socialsignin.spring.data.dynamodb.query.Query;
32  import org.socialsignin.spring.data.dynamodb.repository.support.DynamoDBEntityInformation;
33  import org.socialsignin.spring.data.dynamodb.utils.SortHandler;
34  import org.springframework.data.domain.Sort;
35  import org.springframework.data.domain.Sort.Direction;
36  import org.springframework.data.domain.Sort.Order;
37  import org.springframework.lang.Nullable;
38  import org.springframework.util.Assert;
39  import org.springframework.util.ClassUtils;
40  import org.springframework.util.LinkedMultiValueMap;
41  import org.springframework.util.MultiValueMap;
42  
43  import java.time.Instant;
44  import java.util.ArrayList;
45  import java.util.Arrays;
46  import java.util.Collection;
47  import java.util.Date;
48  import java.util.HashMap;
49  import java.util.HashSet;
50  import java.util.List;
51  import java.util.Map;
52  import java.util.Map.Entry;
53  import java.util.Optional;
54  
55  /**
56   * @author Michael Lavelle
57   * @author Sebastian Just
58   */
59  public abstract class AbstractDynamoDBQueryCriteria<T, ID> implements DynamoDBQueryCriteria<T, ID>, SortHandler {
60  
61  	protected Class<T> clazz;
62  	private DynamoDBEntityInformation<T, ID> entityInformation;
63  	private Map<String, String> attributeNamesByPropertyName;
64  	private final DynamoDBMapperTableModel<T> tableModel;
65  	private String hashKeyPropertyName;
66  
67  	protected MultiValueMap<String, Condition> attributeConditions;
68  	protected MultiValueMap<String, Condition> propertyConditions;
69  
70  	protected Object hashKeyAttributeValue;
71  	protected Object hashKeyPropertyValue;
72  	protected String globalSecondaryIndexName;
73  	protected Sort sort = Sort.unsorted();
74  	protected Optional<String> projection = Optional.empty();
75  
76  	public abstract boolean isApplicableForLoad();
77  
78  	protected QueryRequest buildQueryRequest(String tableName, String theIndexName, String hashKeyAttributeName,
79  			String rangeKeyAttributeName, String rangeKeyPropertyName, List<Condition> hashKeyConditions,
80  			List<Condition> rangeKeyConditions) {
81  
82  		// TODO Set other query request properties based on config
83  		QueryRequest queryRequest = new QueryRequest();
84  		queryRequest.setTableName(tableName);
85  		queryRequest.setIndexName(theIndexName);
86  
87  		if (isApplicableForGlobalSecondaryIndex()) {
88  			List<String> allowedSortProperties = new ArrayList<>();
89  
90  			for (Entry<String, List<Condition>> singlePropertyCondition : propertyConditions.entrySet()) {
91  				if (entityInformation.getGlobalSecondaryIndexNamesByPropertyName().keySet()
92  						.contains(singlePropertyCondition.getKey())) {
93  					allowedSortProperties.add(singlePropertyCondition.getKey());
94  				}
95  			}
96  
97  			HashMap<String, Condition> keyConditions = new HashMap<>();
98  
99  			if (hashKeyConditions != null && hashKeyConditions.size() > 0) {
100 				for (Condition hashKeyCondition : hashKeyConditions) {
101 					keyConditions.put(hashKeyAttributeName, hashKeyCondition);
102 					allowedSortProperties.add(hashKeyPropertyName);
103 				}
104 			}
105 			if (rangeKeyConditions != null && rangeKeyConditions.size() > 0) {
106 				for (Condition rangeKeyCondition : rangeKeyConditions) {
107 					keyConditions.put(rangeKeyAttributeName, rangeKeyCondition);
108 					allowedSortProperties.add(rangeKeyPropertyName);
109 				}
110 			}
111 
112 			for (Entry<String, List<Condition>> singleAttributeConditions : attributeConditions.entrySet()) {
113 
114 				for (Condition condition : singleAttributeConditions.getValue()) {
115 					keyConditions.put(singleAttributeConditions.getKey(), condition);
116 				}
117 			}
118 
119 			for (Order order : sort) {
120 				final String sortProperty = order.getProperty();
121 				if (entityInformation.isGlobalIndexRangeKeyProperty(sortProperty)) {
122 					allowedSortProperties.add(sortProperty);
123 				}
124 			}
125 
126 			queryRequest.setKeyConditions(keyConditions);
127 			// Might be overwritten in the actual Query classes
128 			if (projection.isPresent()) {
129 				queryRequest.setSelect(Select.SPECIFIC_ATTRIBUTES);
130 				queryRequest.setProjectionExpression(projection.get());
131 			} else {
132 				queryRequest.setSelect(Select.ALL_PROJECTED_ATTRIBUTES);
133 			}
134 
135 			applySortIfSpecified(queryRequest, new ArrayList<>(new HashSet<>(allowedSortProperties)));
136 		}
137 		return queryRequest;
138 	}
139 
140 	protected void applySortIfSpecified(DynamoDBQueryExpression<T> queryExpression,
141 			List<String> permittedPropertyNames) {
142 		if (permittedPropertyNames.size() > 1) {
143 			throw new UnsupportedOperationException("Can only sort by at most a single range or index range key");
144 
145 		}
146 
147 		boolean sortAlreadySet = false;
148 		for (Order order : sort) {
149 			if (permittedPropertyNames.contains(order.getProperty())) {
150 				if (sortAlreadySet) {
151 					throw new UnsupportedOperationException("Sorting by multiple attributes not possible");
152 
153 				}
154 				queryExpression.setScanIndexForward(order.getDirection().equals(Direction.ASC));
155 				sortAlreadySet = true;
156 			} else {
157 				throw new UnsupportedOperationException(
158 						"Sorting only possible by " + permittedPropertyNames + " for the criteria specified");
159 			}
160 		}
161 	}
162 
163 	protected void applySortIfSpecified(QueryRequest queryRequest, List<String> permittedPropertyNames) {
164 		if (permittedPropertyNames.size() > 2) {
165 			throw new UnsupportedOperationException("Can only sort by at most a single global hash and range key");
166 		}
167 
168 		boolean sortAlreadySet = false;
169 		for (Order order : sort) {
170 			if (permittedPropertyNames.contains(order.getProperty())) {
171 				if (sortAlreadySet) {
172 					throw new UnsupportedOperationException("Sorting by multiple attributes not possible");
173 
174 				}
175 				if (queryRequest.getKeyConditions().size() > 1 && !hasIndexHashKeyEqualCondition()) {
176 					throw new UnsupportedOperationException(
177 							"Sorting for global index queries with criteria on both hash and range not possible");
178 
179 				}
180 				queryRequest.setScanIndexForward(order.getDirection().equals(Direction.ASC));
181 				sortAlreadySet = true;
182 			} else {
183 				throw new UnsupportedOperationException(
184 						"Sorting only possible by " + permittedPropertyNames + " for the criteria specified");
185 			}
186 		}
187 	}
188 
189 	public boolean comparisonOperatorsPermittedForQuery() {
190 		List<ComparisonOperator> comparisonOperatorsPermittedForQuery = Arrays.asList(new ComparisonOperator[]{
191 				ComparisonOperator.EQ, ComparisonOperator.LE, ComparisonOperator.LT, ComparisonOperator.GE,
192 				ComparisonOperator.GT, ComparisonOperator.BEGINS_WITH, ComparisonOperator.BETWEEN});
193 
194 		// Can only query on subset of Conditions
195 		for (Collection<Condition> conditions : attributeConditions.values()) {
196 			for (Condition condition : conditions) {
197 				if (!comparisonOperatorsPermittedForQuery
198 						.contains(ComparisonOperator.fromValue(condition.getComparisonOperator()))) {
199 					return false;
200 				}
201 			}
202 		}
203 		return true;
204 	}
205 
206 	protected List<Condition> getHashKeyConditions() {
207 		List<Condition> hashKeyConditions = null;
208 		if (isApplicableForGlobalSecondaryIndex() && entityInformation.getGlobalSecondaryIndexNamesByPropertyName()
209 				.keySet().contains(getHashKeyPropertyName())) {
210 			hashKeyConditions = getHashKeyAttributeValue() == null
211 					? null
212 					: Arrays.asList(createSingleValueCondition(getHashKeyPropertyName(), ComparisonOperator.EQ,
213 							getHashKeyAttributeValue(), getHashKeyAttributeValue().getClass(), true));
214 			if (hashKeyConditions == null) {
215 				if (attributeConditions.containsKey(getHashKeyAttributeName())) {
216 					hashKeyConditions = attributeConditions.get(getHashKeyAttributeName());
217 				}
218 
219 			}
220 
221 		}
222 		return hashKeyConditions;
223 	}
224 
225 	public AbstractDynamoDBQueryCriteria(DynamoDBEntityInformation<T, ID> dynamoDBEntityInformation,
226 			final DynamoDBMapperTableModel<T> tableModel) {
227 		this.clazz = dynamoDBEntityInformation.getJavaType();
228 		this.attributeConditions = new LinkedMultiValueMap<>();
229 		this.propertyConditions = new LinkedMultiValueMap<>();
230 		this.hashKeyPropertyName = dynamoDBEntityInformation.getHashKeyPropertyName();
231 		this.entityInformation = dynamoDBEntityInformation;
232 		this.attributeNamesByPropertyName = new HashMap<>();
233 		// TODO consider adding the DynamoDBMapper table model to
234 		// DynamoDBEntityInformation instead
235 		this.tableModel = tableModel;
236 	}
237 
238 	private String getFirstDeclaredIndexNameForAttribute(Map<String, String[]> indexNamesByAttributeName,
239 			List<String> indexNamesToCheck, String attributeName) {
240 		String indexName = null;
241 		String[] declaredOrderedIndexNamesForAttribute = indexNamesByAttributeName.get(attributeName);
242 		for (String declaredOrderedIndexNameForAttribute : declaredOrderedIndexNamesForAttribute) {
243 			if (indexName == null && indexNamesToCheck.contains(declaredOrderedIndexNameForAttribute)) {
244 				indexName = declaredOrderedIndexNameForAttribute;
245 			}
246 		}
247 
248 		return indexName;
249 	}
250 
251 	protected String getGlobalSecondaryIndexName() {
252 
253 		// Lazy evaluate the globalSecondaryIndexName if not already set
254 
255 		// We must have attribute conditions specified in order to use a global
256 		// secondary index, otherwise return null for index name
257 		// Also this method only evaluates the
258 		if (globalSecondaryIndexName == null && attributeConditions != null && !attributeConditions.isEmpty()) {
259 			// Declare map of index names by attribute name which we will populate below -
260 			// this will be used to determine which index to use if multiple indexes are
261 			// applicable
262 			Map<String, String[]> indexNamesByAttributeName = new HashMap<>();
263 
264 			// Declare map of attribute lists by index name which we will populate below -
265 			// this will be used to determine whether we have an exact match index for
266 			// specified attribute conditions
267 			MultiValueMap<String, String> attributeListsByIndexName = new LinkedMultiValueMap<>();
268 
269 			// Populate the above maps
270 			for (Entry<String, String[]> indexNamesForPropertyNameEntry : entityInformation
271 					.getGlobalSecondaryIndexNamesByPropertyName().entrySet()) {
272 				String propertyName = indexNamesForPropertyNameEntry.getKey();
273 				String attributeName = getAttributeName(propertyName);
274 				indexNamesByAttributeName.put(attributeName, indexNamesForPropertyNameEntry.getValue());
275 				for (String indexNameForPropertyName : indexNamesForPropertyNameEntry.getValue()) {
276 					attributeListsByIndexName.add(indexNameForPropertyName, attributeName);
277 				}
278 			}
279 
280 			// Declare lists to store matching index names
281 			List<String> exactMatchIndexNames = new ArrayList<>();
282 			List<String> partialMatchIndexNames = new ArrayList<>();
283 
284 			// Populate matching index name lists - an index is either an exact match ( the
285 			// index attributes match all the specified criteria exactly)
286 			// or a partial match ( the properties for the specified criteria are contained
287 			// within the property set for an index )
288 			for (Entry<String, List<String>> attributeListForIndexNameEntry : attributeListsByIndexName.entrySet()) {
289 				String indexNameForAttributeList = attributeListForIndexNameEntry.getKey();
290 				List<String> attributeList = attributeListForIndexNameEntry.getValue();
291 				if (attributeList.containsAll(attributeConditions.keySet())) {
292 					if (attributeConditions.keySet().containsAll(attributeList)) {
293 						exactMatchIndexNames.add(indexNameForAttributeList);
294 					} else {
295 						partialMatchIndexNames.add(indexNameForAttributeList);
296 					}
297 				}
298 			}
299 
300 			if (exactMatchIndexNames.size() > 1) {
301 				throw new RuntimeException(
302 						"Multiple indexes defined on same attribute set:" + attributeConditions.keySet());
303 			} else if (exactMatchIndexNames.size() == 1) {
304 				globalSecondaryIndexName = exactMatchIndexNames.get(0);
305 			} else if (partialMatchIndexNames.size() > 1) {
306 				if (attributeConditions.size() == 1) {
307 					globalSecondaryIndexName = getFirstDeclaredIndexNameForAttribute(indexNamesByAttributeName,
308 							partialMatchIndexNames, attributeConditions.keySet().iterator().next());
309 				}
310 				if (globalSecondaryIndexName == null) {
311 					globalSecondaryIndexName = partialMatchIndexNames.get(0);
312 				}
313 			} else if (partialMatchIndexNames.size() == 1) {
314 				globalSecondaryIndexName = partialMatchIndexNames.get(0);
315 			}
316 		}
317 		return globalSecondaryIndexName;
318 	}
319 	protected boolean isHashKeyProperty(String propertyName) {
320 		return hashKeyPropertyName.equals(propertyName);
321 	}
322 
323 	protected String getHashKeyPropertyName() {
324 		return hashKeyPropertyName;
325 	}
326 
327 	protected String getHashKeyAttributeName() {
328 		return getAttributeName(getHashKeyPropertyName());
329 	}
330 
331 	protected boolean hasIndexHashKeyEqualCondition() {
332 		boolean hasIndexHashKeyEqualCondition = false;
333 		for (Map.Entry<String, List<Condition>> propertyConditionList : propertyConditions.entrySet()) {
334 			if (entityInformation.isGlobalIndexHashKeyProperty(propertyConditionList.getKey())) {
335 				for (Condition condition : propertyConditionList.getValue()) {
336 					if (condition.getComparisonOperator().equals(ComparisonOperator.EQ.name())) {
337 						hasIndexHashKeyEqualCondition = true;
338 					}
339 				}
340 			}
341 		}
342 		if (hashKeyAttributeValue != null && entityInformation.isGlobalIndexHashKeyProperty(hashKeyPropertyName)) {
343 			hasIndexHashKeyEqualCondition = true;
344 		}
345 		return hasIndexHashKeyEqualCondition;
346 	}
347 
348 	protected boolean hasIndexRangeKeyCondition() {
349 		boolean hasIndexRangeKeyCondition = false;
350 		for (Map.Entry<String, List<Condition>> propertyConditionList : propertyConditions.entrySet()) {
351 			if (entityInformation.isGlobalIndexRangeKeyProperty(propertyConditionList.getKey())) {
352 				hasIndexRangeKeyCondition = true;
353 			}
354 		}
355 		if (hashKeyAttributeValue != null && entityInformation.isGlobalIndexRangeKeyProperty(hashKeyPropertyName)) {
356 			hasIndexRangeKeyCondition = true;
357 		}
358 		return hasIndexRangeKeyCondition;
359 	}
360 	protected boolean isApplicableForGlobalSecondaryIndex() {
361 		boolean global = this.getGlobalSecondaryIndexName() != null;
362 		if (global && getHashKeyAttributeValue() != null && !entityInformation
363 				.getGlobalSecondaryIndexNamesByPropertyName().keySet().contains(getHashKeyPropertyName())) {
364 			return false;
365 		}
366 
367 		int attributeConditionCount = attributeConditions.keySet().size();
368 		boolean attributeConditionsAppropriate = hasIndexHashKeyEqualCondition()
369 				&& (attributeConditionCount == 1 || (attributeConditionCount == 2 && hasIndexRangeKeyCondition()));
370 		return global && (attributeConditionCount == 0 || attributeConditionsAppropriate)
371 				&& comparisonOperatorsPermittedForQuery();
372 
373 	}
374 
375 	public DynamoDBQueryCriteria<T, ID> withHashKeyEquals(Object value) {
376 		Assert.notNull(value, "Creating conditions on null hash keys not supported: please specify a value for '"
377 				+ getHashKeyPropertyName() + "'");
378 
379 		hashKeyAttributeValue = getPropertyAttributeValue(getHashKeyPropertyName(), value);
380 		hashKeyPropertyValue = value;
381 		return this;
382 	}
383 
384 	public boolean isHashKeySpecified() {
385 		return getHashKeyAttributeValue() != null;
386 	}
387 
388 	public Object getHashKeyAttributeValue() {
389 		return hashKeyAttributeValue;
390 	}
391 
392 	public Object getHashKeyPropertyValue() {
393 		return hashKeyPropertyValue;
394 	}
395 
396 	protected String getAttributeName(String propertyName) {
397 		String attributeName = attributeNamesByPropertyName.get(propertyName);
398 		if (attributeName == null) {
399 			attributeName = entityInformation.getOverriddenAttributeName(propertyName).orElse(propertyName);
400 			attributeNamesByPropertyName.put(propertyName, attributeName);
401 		}
402 		return attributeName;
403 
404 	}
405 
406 	@Override
407 	public DynamoDBQueryCriteria<T, ID> withPropertyBetween(String propertyName, Object value1, Object value2,
408 			Class<?> type) {
409 		Condition condition = createCollectionCondition(propertyName, ComparisonOperator.BETWEEN,
410 				Arrays.asList(value1, value2), type);
411 		return withCondition(propertyName, condition);
412 	}
413 
414 	@Override
415 	public DynamoDBQueryCriteria<T, ID> withPropertyIn(String propertyName, Iterable<?> value, Class<?> propertyType) {
416 
417 		Condition condition = createCollectionCondition(propertyName, ComparisonOperator.IN, value, propertyType);
418 		return withCondition(propertyName, condition);
419 	}
420 
421 	@Override
422 	public DynamoDBQueryCriteria<T, ID> withSingleValueCriteria(String propertyName,
423 			ComparisonOperator comparisonOperator, Object value, Class<?> propertyType) {
424 		if (comparisonOperator.equals(ComparisonOperator.EQ)) {
425 			return withPropertyEquals(propertyName, value, propertyType);
426 		} else {
427 			Condition condition = createSingleValueCondition(propertyName, comparisonOperator, value, propertyType,
428 					false);
429 			return withCondition(propertyName, condition);
430 		}
431 	}
432 
433 	@Override
434 	public Query<T> buildQuery(DynamoDBOperations dynamoDBOperations) {
435 		if (isApplicableForLoad()) {
436 			return buildSingleEntityLoadQuery(dynamoDBOperations);
437 		} else {
438 			return buildFinderQuery(dynamoDBOperations);
439 		}
440 	}
441 
442 	@Override
443 	public Query<Long> buildCountQuery(DynamoDBOperations dynamoDBOperations, boolean pageQuery) {
444 		if (isApplicableForLoad()) {
445 			return buildSingleEntityCountQuery(dynamoDBOperations);
446 		} else {
447 			return buildFinderCountQuery(dynamoDBOperations, pageQuery);
448 		}
449 	}
450 
451 	protected abstract Query<T> buildSingleEntityLoadQuery(DynamoDBOperations dynamoDBOperations);
452 
453 	protected abstract Query<Long> buildSingleEntityCountQuery(DynamoDBOperations dynamoDBOperations);
454 
455 	protected abstract Query<T> buildFinderQuery(DynamoDBOperations dynamoDBOperations);
456 
457 	protected abstract Query<Long> buildFinderCountQuery(DynamoDBOperations dynamoDBOperations, boolean pageQuery);
458 
459 	protected abstract boolean isOnlyHashKeySpecified();
460 
461 	@Override
462 	public DynamoDBQueryCriteria<T, ID> withNoValuedCriteria(String propertyName,
463 			ComparisonOperator comparisonOperator) {
464 		Condition condition = createNoValueCondition(propertyName, comparisonOperator);
465 		return withCondition(propertyName, condition);
466 
467 	}
468 
469 	public DynamoDBQueryCriteria<T, ID> withCondition(String propertyName, Condition condition) {
470 		attributeConditions.add(getAttributeName(propertyName), condition);
471 		propertyConditions.add(propertyName, condition);
472 
473 		return this;
474 	}
475 
476 	@SuppressWarnings({"deprecation", "unchecked"})
477 	protected <V extends Object> Object getPropertyAttributeValue(final String propertyName, final V value) {
478 		// TODO consider removing DynamoDBMarshaller code altogether as table model will
479 		// handle accordingly
480 		DynamoDBTypeConverter<Object, V> converter = (DynamoDBTypeConverter<Object, V>) entityInformation
481 				.getTypeConverterForProperty(propertyName);
482 
483 		if (converter != null) {
484 			return converter.convert(value);
485 		}
486 
487 		DynamoDBMarshaller<V> marshaller = (DynamoDBMarshaller<V>) entityInformation
488 				.getMarshallerForProperty(propertyName);
489 
490 		if (marshaller != null) {
491 			return marshaller.marshall(value);
492 		} else if (tableModel != null) { // purely here for testing as DynamoDBMapperTableModel cannot be mocked using
493 											// Mockito
494 
495 			String attributeName = getAttributeName(propertyName);
496 
497 			DynamoDBMapperFieldModel<T, Object> fieldModel = tableModel.field(attributeName);
498 			if (fieldModel != null) {
499 				return fieldModel.convert(value);
500 			}
501 		}
502 
503 		return value;
504 	}
505 
506 	protected <V> Condition createNoValueCondition(String propertyName, ComparisonOperator comparisonOperator) {
507 
508 		Condition condition = new Condition().withComparisonOperator(comparisonOperator);
509 
510 		return condition;
511 	}
512 
513 	private List<String> getNumberListAsStringList(List<Number> numberList) {
514 		List<String> list = new ArrayList<>();
515 		for (Number number : numberList) {
516 			if (number != null) {
517 				list.add(number.toString());
518 			} else {
519 				list.add(null);
520 			}
521 		}
522 		return list;
523 	}
524 
525 	@SuppressWarnings("deprecation")
526 	private List<String> getDateListAsStringList(List<Date> dateList) {
527 		DynamoDBMarshaller<Date> marshaller = new Date2IsoDynamoDBMarshaller();
528 		List<String> list = new ArrayList<String>();
529 		for (Date date : dateList) {
530 			if (date != null) {
531 				list.add(marshaller.marshall(date));
532 			} else {
533 				list.add(null);
534 			}
535 		}
536 		return list;
537 	}
538 
539 	@SuppressWarnings("deprecation")
540 	private List<String> getInstantListAsStringList(List<Instant> dateList) {
541 		DynamoDBMarshaller<Instant> marshaller = new Instant2IsoDynamoDBMarshaller();
542 		List<String> list = new ArrayList<>();
543 		for (Instant date : dateList) {
544 			if (date != null) {
545 				list.add(marshaller.marshall(date));
546 			} else {
547 				list.add(null);
548 			}
549 		}
550 		return list;
551 	}
552 
553 	private List<String> getBooleanListAsStringList(List<Boolean> booleanList) {
554 		List<String> list = new ArrayList<>();
555 		for (Boolean booleanValue : booleanList) {
556 			if (booleanValue != null) {
557 				list.add(booleanValue.booleanValue() ? "1" : "0");
558 			} else {
559 				list.add(null);
560 			}
561 		}
562 		return list;
563 	}
564 
565 	@SuppressWarnings("unchecked")
566 	@Nullable
567 	private <P> List<P> getAttributeValueAsList(@Nullable Object attributeValue) {
568 		if (attributeValue == null) {
569 			return null;
570 		}
571 		boolean isIterable = ClassUtils.isAssignable(Iterable.class, attributeValue.getClass());
572 		if (isIterable) {
573 			List<P> attributeValueAsList = new ArrayList<>();
574 			Iterable<P> iterable = (Iterable<P>) attributeValue;
575 			for (P attributeValueElement : iterable) {
576 				attributeValueAsList.add(attributeValueElement);
577 			}
578 			return attributeValueAsList;
579 		}
580 		return null;
581 	}
582 
583 	protected <P> List<AttributeValue> addAttributeValue(List<AttributeValue> attributeValueList,
584 			@Nullable Object attributeValue, Class<P> propertyType, boolean expandCollectionValues) {
585 		AttributeValue attributeValueObject = new AttributeValue();
586 
587 		if (ClassUtils.isAssignable(String.class, propertyType)) {
588 			List<String> attributeValueAsList = getAttributeValueAsList(attributeValue);
589 			if (expandCollectionValues && attributeValueAsList != null) {
590 				attributeValueObject.withSS(attributeValueAsList);
591 			} else {
592 				attributeValueObject.withS((String) attributeValue);
593 			}
594 		} else if (ClassUtils.isAssignable(Number.class, propertyType)) {
595 
596 			List<Number> attributeValueAsList = getAttributeValueAsList(attributeValue);
597 			if (expandCollectionValues && attributeValueAsList != null) {
598 				List<String> attributeValueAsStringList = getNumberListAsStringList(attributeValueAsList);
599 				attributeValueObject.withNS(attributeValueAsStringList);
600 			} else {
601 				attributeValueObject.withN(attributeValue.toString());
602 			}
603 		} else if (ClassUtils.isAssignable(Boolean.class, propertyType)) {
604 			List<Boolean> attributeValueAsList = getAttributeValueAsList(attributeValue);
605 			if (expandCollectionValues && attributeValueAsList != null) {
606 				List<String> attributeValueAsStringList = getBooleanListAsStringList(attributeValueAsList);
607 				attributeValueObject.withNS(attributeValueAsStringList);
608 			} else {
609 				boolean boolValue = ((Boolean) attributeValue).booleanValue();
610 				attributeValueObject.withN(boolValue ? "1" : "0");
611 			}
612 		} else if (ClassUtils.isAssignable(Date.class, propertyType)) {
613 			List<Date> attributeValueAsList = getAttributeValueAsList(attributeValue);
614 			if (expandCollectionValues && attributeValueAsList != null) {
615 				List<String> attributeValueAsStringList = getDateListAsStringList(attributeValueAsList);
616 				attributeValueObject.withSS(attributeValueAsStringList);
617 			} else {
618 				Date date = (Date) attributeValue;
619 				String marshalledDate = new Date2IsoDynamoDBMarshaller().marshall(date);
620 				attributeValueObject.withS(marshalledDate);
621 			}
622 		} else if (ClassUtils.isAssignable(Instant.class, propertyType)) {
623 			List<Instant> attributeValueAsList = getAttributeValueAsList(attributeValue);
624 			if (expandCollectionValues && attributeValueAsList != null) {
625 				List<String> attributeValueAsStringList = getInstantListAsStringList(attributeValueAsList);
626 				attributeValueObject.withSS(attributeValueAsStringList);
627 			} else {
628 				Instant date = (Instant) attributeValue;
629 				String marshalledDate = new Instant2IsoDynamoDBMarshaller().marshall(date);
630 				attributeValueObject.withS(marshalledDate);
631 			}
632 		} else {
633 			throw new RuntimeException("Cannot create condition for type:" + attributeValue.getClass()
634 					+ " property conditions must be String,Number or Boolean, or have a DynamoDBMarshaller configured");
635 		}
636 		attributeValueList.add(attributeValueObject);
637 
638 		return attributeValueList;
639 	}
640 
641 	protected Condition createSingleValueCondition(String propertyName, ComparisonOperator comparisonOperator, Object o,
642 			Class<?> propertyType, boolean alreadyMarshalledIfRequired) {
643 
644 		Assert.notNull(o, "Creating conditions on null property values not supported: please specify a value for '"
645 				+ propertyName + "'");
646 
647 		List<AttributeValue> attributeValueList = new ArrayList<>(1);
648 		Object attributeValue = !alreadyMarshalledIfRequired ? getPropertyAttributeValue(propertyName, o) : o;
649 		if (ClassUtils.isAssignableValue(AttributeValue.class, attributeValue)) {
650 			attributeValueList.add((AttributeValue) attributeValue);
651 		} else {
652 			boolean marshalled = !alreadyMarshalledIfRequired && attributeValue != o
653 					&& !entityInformation.isCompositeHashAndRangeKeyProperty(propertyName);
654 
655 			Class<?> targetPropertyType = marshalled ? String.class : propertyType;
656 			addAttributeValue(attributeValueList, attributeValue, targetPropertyType, true);
657 		}
658 
659 		return new Condition().withComparisonOperator(comparisonOperator).withAttributeValueList(attributeValueList);
660 	}
661 
662 	protected Condition createCollectionCondition(String propertyName, ComparisonOperator comparisonOperator,
663 			Iterable<?> o, Class<?> propertyType) {
664 
665 		Assert.notNull(o, "Creating conditions on null property values not supported: please specify a value for '"
666 				+ propertyName + "'");
667 		List<AttributeValue> attributeValueList = new ArrayList<>();
668 		boolean marshalled = false;
669 		for (Object object : o) {
670 			Object attributeValue = getPropertyAttributeValue(propertyName, object);
671 			if (ClassUtils.isAssignableValue(AttributeValue.class, attributeValue)) {
672 				attributeValueList.add((AttributeValue) attributeValue);
673 			} else {
674 				if (attributeValue != null) {
675 					marshalled = attributeValue != object
676 							&& !entityInformation.isCompositeHashAndRangeKeyProperty(propertyName);
677 				}
678 				Class<?> targetPropertyType = marshalled ? String.class : propertyType;
679 				addAttributeValue(attributeValueList, attributeValue, targetPropertyType, false);
680 			}
681 		}
682 
683 		return new Condition().withComparisonOperator(comparisonOperator).withAttributeValueList(attributeValueList);
684 
685 	}
686 
687 	@Override
688 	public DynamoDBQueryCriteria<T, ID> withSort(Sort sort) {
689 		this.sort = sort;
690 		return this;
691 	}
692 
693 	@Override
694 	public DynamoDBQueryCriteria<T, ID> withProjection(Optional<String> projection) {
695 		this.projection = projection;
696 		return this;
697 	}
698 }