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.DynamoDBMapperTableModel;
19  import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBQueryExpression;
20  import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBScanExpression;
21  import com.amazonaws.services.dynamodbv2.model.ComparisonOperator;
22  import com.amazonaws.services.dynamodbv2.model.Condition;
23  import com.amazonaws.services.dynamodbv2.model.QueryRequest;
24  import com.amazonaws.services.dynamodbv2.model.Select;
25  import org.socialsignin.spring.data.dynamodb.core.DynamoDBOperations;
26  import org.socialsignin.spring.data.dynamodb.query.CountByHashAndRangeKeyQuery;
27  import org.socialsignin.spring.data.dynamodb.query.MultipleEntityQueryExpressionQuery;
28  import org.socialsignin.spring.data.dynamodb.query.MultipleEntityQueryRequestQuery;
29  import org.socialsignin.spring.data.dynamodb.query.MultipleEntityScanExpressionQuery;
30  import org.socialsignin.spring.data.dynamodb.query.Query;
31  import org.socialsignin.spring.data.dynamodb.query.QueryExpressionCountQuery;
32  import org.socialsignin.spring.data.dynamodb.query.QueryRequestCountQuery;
33  import org.socialsignin.spring.data.dynamodb.query.ScanExpressionCountQuery;
34  import org.socialsignin.spring.data.dynamodb.query.SingleEntityLoadByHashAndRangeKeyQuery;
35  import org.socialsignin.spring.data.dynamodb.repository.support.DynamoDBIdIsHashAndRangeKeyEntityInformation;
36  import org.springframework.util.Assert;
37  
38  import java.util.ArrayList;
39  import java.util.Arrays;
40  import java.util.HashMap;
41  import java.util.HashSet;
42  import java.util.List;
43  import java.util.Map;
44  import java.util.Map.Entry;
45  import java.util.Set;
46  
47  /**
48   * @author Michael Lavelle
49   * @author Sebastian Just
50   */
51  public class DynamoDBEntityWithHashAndRangeKeyCriteria<T, ID> extends AbstractDynamoDBQueryCriteria<T, ID> {
52  
53  	private Object rangeKeyAttributeValue;
54  	private Object rangeKeyPropertyValue;
55  	private String rangeKeyPropertyName;
56  	private Set<String> indexRangeKeyPropertyNames;
57  	private DynamoDBIdIsHashAndRangeKeyEntityInformation<T, ID> entityInformation;
58  
59  	protected String getRangeKeyAttributeName() {
60  		return getAttributeName(getRangeKeyPropertyName());
61  	}
62  
63  	protected String getRangeKeyPropertyName() {
64  		return rangeKeyPropertyName;
65  	}
66  
67  	protected boolean isRangeKeyProperty(String propertyName) {
68  		return rangeKeyPropertyName.equals(propertyName);
69  	}
70  
71  	public DynamoDBEntityWithHashAndRangeKeyCriteria(
72  			DynamoDBIdIsHashAndRangeKeyEntityInformation<T, ID> entityInformation,
73  			DynamoDBMapperTableModel<T> tableModel) {
74  
75  		super(entityInformation, tableModel);
76  		this.rangeKeyPropertyName = entityInformation.getRangeKeyPropertyName();
77  		this.indexRangeKeyPropertyNames = entityInformation.getIndexRangeKeyPropertyNames();
78  		if (indexRangeKeyPropertyNames == null) {
79  			indexRangeKeyPropertyNames = new HashSet<>();
80  		}
81  		this.entityInformation = entityInformation;
82  	}
83  
84  	public Set<String> getIndexRangeKeyAttributeNames() {
85  		Set<String> indexRangeKeyAttributeNames = new HashSet<>();
86  		for (String indexRangeKeyPropertyName : indexRangeKeyPropertyNames) {
87  			indexRangeKeyAttributeNames.add(getAttributeName(indexRangeKeyPropertyName));
88  		}
89  		return indexRangeKeyAttributeNames;
90  	}
91  
92  	protected Object getRangeKeyAttributeValue() {
93  		return rangeKeyAttributeValue;
94  	}
95  
96  	protected Object getRangeKeyPropertyValue() {
97  		return rangeKeyPropertyValue;
98  	}
99  
100 	protected boolean isRangeKeySpecified() {
101 		return getRangeKeyAttributeValue() != null;
102 	}
103 
104 	protected Query<T> buildSingleEntityLoadQuery(DynamoDBOperations dynamoDBOperations) {
105 		return new SingleEntityLoadByHashAndRangeKeyQuery<>(dynamoDBOperations, entityInformation.getJavaType(),
106 				getHashKeyPropertyValue(), getRangeKeyPropertyValue());
107 	}
108 
109 	protected Query<Long> buildSingleEntityCountQuery(DynamoDBOperations dynamoDBOperations) {
110 		return new CountByHashAndRangeKeyQuery<>(dynamoDBOperations, entityInformation.getJavaType(),
111 				getHashKeyPropertyValue(), getRangeKeyPropertyValue());
112 	}
113 
114 	private void checkComparisonOperatorPermittedForCompositeHashAndRangeKey(ComparisonOperator comparisonOperator) {
115 
116 		if (!ComparisonOperator.EQ.equals(comparisonOperator) && !ComparisonOperator.CONTAINS.equals(comparisonOperator)
117 				&& !ComparisonOperator.BEGINS_WITH.equals(comparisonOperator)) {
118 			throw new UnsupportedOperationException(
119 					"Only EQ,CONTAINS,BEGINS_WITH supported for composite id comparison");
120 		}
121 
122 	}
123 
124 	@SuppressWarnings("unchecked")
125 	@Override
126 	public DynamoDBQueryCriteria<T, ID> withSingleValueCriteria(String propertyName,
127 			ComparisonOperator comparisonOperator, Object value, Class<?> propertyType) {
128 
129 		if (entityInformation.isCompositeHashAndRangeKeyProperty(propertyName)) {
130 			checkComparisonOperatorPermittedForCompositeHashAndRangeKey(comparisonOperator);
131 			Object hashKey = entityInformation.getHashKey((ID) value);
132 			Object rangeKey = entityInformation.getRangeKey((ID) value);
133 			if (hashKey != null) {
134 				withSingleValueCriteria(getHashKeyPropertyName(), comparisonOperator, hashKey, hashKey.getClass());
135 			}
136 			if (rangeKey != null) {
137 				withSingleValueCriteria(getRangeKeyPropertyName(), comparisonOperator, rangeKey, rangeKey.getClass());
138 			}
139 			return this;
140 		} else {
141 			return super.withSingleValueCriteria(propertyName, comparisonOperator, value, propertyType);
142 		}
143 	}
144 
145 	public DynamoDBQueryExpression<T> buildQueryExpression() {
146 		DynamoDBQueryExpression<T> queryExpression = new DynamoDBQueryExpression<T>();
147 		if (isHashKeySpecified()) {
148 			T hashKeyPrototype = entityInformation.getHashKeyPropotypeEntityForHashKey(getHashKeyPropertyValue());
149 			queryExpression.withHashKeyValues(hashKeyPrototype);
150 			queryExpression.withRangeKeyConditions(new HashMap<String, Condition>());
151 		}
152 
153 		if (isRangeKeySpecified() && !isApplicableForGlobalSecondaryIndex()) {
154 			Condition rangeKeyCondition = createSingleValueCondition(getRangeKeyPropertyName(), ComparisonOperator.EQ,
155 					getRangeKeyAttributeValue(), getRangeKeyAttributeValue().getClass(), true);
156 			queryExpression.withRangeKeyCondition(getRangeKeyAttributeName(), rangeKeyCondition);
157 			applySortIfSpecified(queryExpression, Arrays.asList(new String[]{getRangeKeyPropertyName()}));
158 
159 		} else if (isOnlyASingleAttributeConditionAndItIsOnEitherRangeOrIndexRangeKey()
160 				|| (isApplicableForGlobalSecondaryIndex())) {
161 
162 			Entry<String, List<Condition>> singlePropertyConditions = propertyConditions.entrySet().iterator().next();
163 
164 			List<String> allowedSortProperties = new ArrayList<>();
165 			for (Entry<String, List<Condition>> singlePropertyCondition : propertyConditions.entrySet()) {
166 				if (entityInformation.getGlobalSecondaryIndexNamesByPropertyName().keySet()
167 						.contains(singlePropertyCondition.getKey())) {
168 					allowedSortProperties.add(singlePropertyCondition.getKey());
169 				}
170 			}
171 			if (allowedSortProperties.size() == 0) {
172 				allowedSortProperties.add(singlePropertyConditions.getKey());
173 			}
174 
175 			for (Entry<String, List<Condition>> singleAttributeConditions : attributeConditions.entrySet()) {
176 				for (Condition condition : singleAttributeConditions.getValue()) {
177 					queryExpression.withRangeKeyCondition(singleAttributeConditions.getKey(), condition);
178 				}
179 			}
180 
181 			applySortIfSpecified(queryExpression, allowedSortProperties);
182 			if (getGlobalSecondaryIndexName() != null) {
183 				queryExpression.setIndexName(getGlobalSecondaryIndexName());
184 			}
185 		} else {
186 			applySortIfSpecified(queryExpression, Arrays.asList(new String[]{getRangeKeyPropertyName()}));
187 		}
188 
189 		if (projection.isPresent()) {
190 			queryExpression.setSelect(Select.SPECIFIC_ATTRIBUTES);
191 			queryExpression.setProjectionExpression(projection.get());
192 		}
193 
194 		return queryExpression;
195 	}
196 
197 	protected List<Condition> getRangeKeyConditions() {
198 		List<Condition> rangeKeyConditions = null;
199 		if (isApplicableForGlobalSecondaryIndex() && entityInformation.getGlobalSecondaryIndexNamesByPropertyName()
200 				.keySet().contains(getRangeKeyPropertyName())) {
201 			rangeKeyConditions = getRangeKeyAttributeValue() == null
202 					? null
203 					: Arrays.asList(createSingleValueCondition(getRangeKeyPropertyName(), ComparisonOperator.EQ,
204 							getRangeKeyAttributeValue(), getRangeKeyAttributeValue().getClass(), true));
205 
206 		}
207 		return rangeKeyConditions;
208 	}
209 
210 	protected Query<T> buildFinderQuery(DynamoDBOperations dynamoDBOperations) {
211 		if (isApplicableForQuery()) {
212 			if (isApplicableForGlobalSecondaryIndex()) {
213 				String tableName = dynamoDBOperations.getOverriddenTableName(clazz,
214 						entityInformation.getDynamoDBTableName());
215 				QueryRequest queryRequest = buildQueryRequest(tableName, getGlobalSecondaryIndexName(),
216 						getHashKeyAttributeName(), getRangeKeyAttributeName(), this.getRangeKeyPropertyName(),
217 						getHashKeyConditions(), getRangeKeyConditions());
218 				return new MultipleEntityQueryRequestQuery<>(dynamoDBOperations, entityInformation.getJavaType(),
219 						queryRequest);
220 			} else {
221 				DynamoDBQueryExpression<T> queryExpression = buildQueryExpression();
222 				return new MultipleEntityQueryExpressionQuery<>(dynamoDBOperations, entityInformation.getJavaType(),
223 						queryExpression);
224 			}
225 		} else {
226 			return new MultipleEntityScanExpressionQuery<>(dynamoDBOperations, clazz, buildScanExpression());
227 		}
228 	}
229 
230 	protected Query<Long> buildFinderCountQuery(DynamoDBOperations dynamoDBOperations, boolean pageQuery) {
231 		if (isApplicableForQuery()) {
232 			if (isApplicableForGlobalSecondaryIndex()) {
233 				String tableName = dynamoDBOperations.getOverriddenTableName(clazz,
234 						entityInformation.getDynamoDBTableName());
235 				QueryRequest queryRequest = buildQueryRequest(tableName, getGlobalSecondaryIndexName(),
236 						getHashKeyAttributeName(), getRangeKeyAttributeName(), this.getRangeKeyPropertyName(),
237 						getHashKeyConditions(), getRangeKeyConditions());
238 				return new QueryRequestCountQuery(dynamoDBOperations, queryRequest);
239 
240 			} else {
241 				DynamoDBQueryExpression<T> queryExpression = buildQueryExpression();
242 				return new QueryExpressionCountQuery<>(dynamoDBOperations, entityInformation.getJavaType(),
243 						queryExpression);
244 
245 			}
246 		} else {
247 			return new ScanExpressionCountQuery<T>(dynamoDBOperations, clazz, buildScanExpression(), pageQuery);
248 		}
249 	}
250 
251 	@Override
252 	public boolean isApplicableForLoad() {
253 		return attributeConditions.size() == 0 && isHashAndRangeKeySpecified();
254 	}
255 
256 	protected boolean isHashAndRangeKeySpecified() {
257 		return isHashKeySpecified() && isRangeKeySpecified();
258 	}
259 
260 	protected boolean isOnlyASingleAttributeConditionAndItIsOnEitherRangeOrIndexRangeKey() {
261 		boolean isOnlyASingleAttributeConditionAndItIsOnEitherRangeOrIndexRangeKey = false;
262 		if (!isRangeKeySpecified() && attributeConditions.size() == 1) {
263 			Entry<String, List<Condition>> conditionsEntry = attributeConditions.entrySet().iterator().next();
264 			if (conditionsEntry.getKey().equals(getRangeKeyAttributeName())
265 					|| getIndexRangeKeyAttributeNames().contains(conditionsEntry.getKey())) {
266 				if (conditionsEntry.getValue().size() == 1) {
267 					isOnlyASingleAttributeConditionAndItIsOnEitherRangeOrIndexRangeKey = true;
268 				}
269 			}
270 		}
271 		return isOnlyASingleAttributeConditionAndItIsOnEitherRangeOrIndexRangeKey;
272 
273 	}
274 
275 	@Override
276 	protected boolean hasIndexHashKeyEqualCondition() {
277 
278 		boolean hasCondition = super.hasIndexHashKeyEqualCondition();
279 		if (!hasCondition) {
280 			if (rangeKeyAttributeValue != null
281 					&& entityInformation.isGlobalIndexHashKeyProperty(rangeKeyPropertyName)) {
282 				hasCondition = true;
283 			}
284 		}
285 		return hasCondition;
286 	}
287 
288 	@Override
289 	protected boolean hasIndexRangeKeyCondition() {
290 		boolean hasCondition = super.hasIndexRangeKeyCondition();
291 		if (!hasCondition) {
292 			if (rangeKeyAttributeValue != null
293 					&& entityInformation.isGlobalIndexRangeKeyProperty(rangeKeyPropertyName)) {
294 				hasCondition = true;
295 			}
296 		}
297 		return hasCondition;
298 	}
299 
300 	protected boolean isApplicableForGlobalSecondaryIndex() {
301 		boolean global = super.isApplicableForGlobalSecondaryIndex();
302 		if (global && getRangeKeyAttributeValue() != null && !entityInformation
303 				.getGlobalSecondaryIndexNamesByPropertyName().keySet().contains(getRangeKeyPropertyName())) {
304 			return false;
305 		}
306 
307 		return global;
308 
309 	}
310 
311 	protected String getGlobalSecondaryIndexName() {
312 		// Get the target global secondary index name using the property
313 		// conditions
314 		String globalSecondaryIndexName = super.getGlobalSecondaryIndexName();
315 
316 		// Hash and Range Entities store range key equals conditions as
317 		// rangeKeyAttributeValue attribute instead of as property condition
318 		// Check this attribute and if specified in the query conditions and
319 		// it's the only global secondary index range candidate,
320 		// then set the index range key to be that associated with the range key
321 		if (globalSecondaryIndexName == null) {
322 			if (this.hashKeyAttributeValue == null && getRangeKeyAttributeValue() != null) {
323 				String[] rangeKeyIndexNames = entityInformation.getGlobalSecondaryIndexNamesByPropertyName()
324 						.get(this.getRangeKeyPropertyName());
325 				globalSecondaryIndexName = rangeKeyIndexNames != null && rangeKeyIndexNames.length > 0
326 						? rangeKeyIndexNames[0]
327 						: null;
328 			}
329 		}
330 		return globalSecondaryIndexName;
331 	}
332 
333 	public boolean isApplicableForQuery() {
334 
335 		return isOnlyHashKeySpecified()
336 				|| (isHashKeySpecified() && isOnlyASingleAttributeConditionAndItIsOnEitherRangeOrIndexRangeKey()
337 						&& comparisonOperatorsPermittedForQuery())
338 				|| isApplicableForGlobalSecondaryIndex();
339 
340 	}
341 
342 	public DynamoDBScanExpression buildScanExpression() {
343 
344 		ensureNoSort(sort);
345 
346 		DynamoDBScanExpression scanExpression = new DynamoDBScanExpression();
347 		if (isHashKeySpecified()) {
348 			scanExpression.addFilterCondition(getHashKeyAttributeName(),
349 					createSingleValueCondition(getHashKeyPropertyName(), ComparisonOperator.EQ,
350 							getHashKeyAttributeValue(), getHashKeyAttributeValue().getClass(), true));
351 		}
352 		if (isRangeKeySpecified()) {
353 			scanExpression.addFilterCondition(getRangeKeyAttributeName(),
354 					createSingleValueCondition(getRangeKeyPropertyName(), ComparisonOperator.EQ,
355 							getRangeKeyAttributeValue(), getRangeKeyAttributeValue().getClass(), true));
356 		}
357 		for (Map.Entry<String, List<Condition>> conditionEntry : attributeConditions.entrySet()) {
358 			for (Condition condition : conditionEntry.getValue()) {
359 				scanExpression.addFilterCondition(conditionEntry.getKey(), condition);
360 			}
361 		}
362 		return scanExpression;
363 	}
364 
365 	public DynamoDBQueryCriteria<T, ID> withRangeKeyEquals(Object value) {
366 		Assert.notNull(value, "Creating conditions on null range keys not supported: please specify a value for '"
367 				+ getRangeKeyPropertyName() + "'");
368 
369 		rangeKeyAttributeValue = getPropertyAttributeValue(getRangeKeyPropertyName(), value);
370 		rangeKeyPropertyValue = value;
371 		return this;
372 	}
373 
374 	@SuppressWarnings("unchecked")
375 	@Override
376 	public DynamoDBQueryCriteria<T, ID> withPropertyEquals(String propertyName, Object value, Class<?> propertyType) {
377 		if (isHashKeyProperty(propertyName)) {
378 			return withHashKeyEquals(value);
379 		} else if (isRangeKeyProperty(propertyName)) {
380 			return withRangeKeyEquals(value);
381 		} else if (entityInformation.isCompositeHashAndRangeKeyProperty(propertyName)) {
382 			Assert.notNull(value,
383 					"Creating conditions on null composite id properties not supported: please specify a value for '"
384 							+ propertyName + "'");
385 			Object hashKey = entityInformation.getHashKey((ID) value);
386 			Object rangeKey = entityInformation.getRangeKey((ID) value);
387 			if (hashKey != null) {
388 				withHashKeyEquals(hashKey);
389 			}
390 			if (rangeKey != null) {
391 				withRangeKeyEquals(rangeKey);
392 			}
393 			return this;
394 		} else {
395 			Condition condition = createSingleValueCondition(propertyName, ComparisonOperator.EQ, value, propertyType,
396 					false);
397 			return withCondition(propertyName, condition);
398 		}
399 
400 	}
401 
402 	@Override
403 	protected boolean isOnlyHashKeySpecified() {
404 		return isHashKeySpecified() && attributeConditions.size() == 0 && !isRangeKeySpecified();
405 	}
406 
407 }