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.support;
17  
18  import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBAttribute;
19  import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBHashKey;
20  import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBIndexHashKey;
21  import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBIndexRangeKey;
22  import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMarshaller;
23  import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMarshalling;
24  import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBRangeKey;
25  import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTable;
26  import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTypeConverted;
27  import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTypeConverter;
28  import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBVersionAttribute;
29  import org.springframework.util.Assert;
30  import org.springframework.util.ReflectionUtils;
31  import org.springframework.util.StringUtils;
32  
33  import java.lang.annotation.Annotation;
34  import java.lang.reflect.Field;
35  import java.lang.reflect.InvocationTargetException;
36  import java.lang.reflect.Method;
37  import java.util.ArrayList;
38  import java.util.HashMap;
39  import java.util.List;
40  import java.util.Map;
41  import java.util.Optional;
42  
43  /**
44   * @author Michael Lavelle
45   * @author Sebastian Just
46   */
47  public class DynamoDBEntityMetadataSupport<T, ID> implements DynamoDBHashKeyExtractingEntityMetadata<T> {
48  
49  	private final Class<T> domainType;
50  	private boolean hasRangeKey;
51  	private String hashKeyPropertyName;
52  	private List<String> globalIndexHashKeyPropertyNames;
53  	private List<String> globalIndexRangeKeyPropertyNames;
54  
55  	private String dynamoDBTableName;
56  	private Map<String, String[]> globalSecondaryIndexNames = new HashMap<>();
57  
58  	@Override
59  	public String getDynamoDBTableName() {
60  		return dynamoDBTableName;
61  	}
62  
63  	/**
64  	 * Creates a new {@link DynamoDBEntityMetadataSupport} for the given domain
65  	 * type.
66  	 *
67  	 * @param domainType
68  	 *            must not be {@literal null}.
69  	 */
70  	public DynamoDBEntityMetadataSupport(final Class<T> domainType) {
71  
72  		Assert.notNull(domainType, "Domain type must not be null!");
73  		this.domainType = domainType;
74  
75  		DynamoDBTable table = this.domainType.getAnnotation(DynamoDBTable.class);
76  		Assert.notNull(table, "Domain type must by annotated with DynamoDBTable!");
77  
78  		this.dynamoDBTableName = table.tableName();
79  		this.hashKeyPropertyName = null;
80  		this.globalSecondaryIndexNames = new HashMap<>();
81  		this.globalIndexHashKeyPropertyNames = new ArrayList<>();
82  		this.globalIndexRangeKeyPropertyNames = new ArrayList<>();
83  		ReflectionUtils.doWithMethods(domainType, method -> {
84  			if (method.getAnnotation(DynamoDBHashKey.class) != null) {
85  				hashKeyPropertyName = getPropertyNameForAccessorMethod(method);
86  			}
87  			if (method.getAnnotation(DynamoDBRangeKey.class) != null) {
88  				hasRangeKey = true;
89  			}
90  			DynamoDBIndexRangeKey dynamoDBRangeKeyAnnotation = method.getAnnotation(DynamoDBIndexRangeKey.class);
91  			DynamoDBIndexHashKey dynamoDBHashKeyAnnotation = method.getAnnotation(DynamoDBIndexHashKey.class);
92  
93  			if (dynamoDBRangeKeyAnnotation != null) {
94  				addGlobalSecondaryIndexNames(method, dynamoDBRangeKeyAnnotation);
95  			}
96  			if (dynamoDBHashKeyAnnotation != null) {
97  				addGlobalSecondaryIndexNames(method, dynamoDBHashKeyAnnotation);
98  			}
99  		});
100 		ReflectionUtils.doWithFields(domainType, field -> {
101 			if (field.getAnnotation(DynamoDBHashKey.class) != null) {
102 				hashKeyPropertyName = getPropertyNameForField(field);
103 			}
104 			if (field.getAnnotation(DynamoDBRangeKey.class) != null) {
105 				hasRangeKey = true;
106 			}
107 			DynamoDBIndexRangeKey dynamoDBRangeKeyAnnotation = field.getAnnotation(DynamoDBIndexRangeKey.class);
108 			DynamoDBIndexHashKey dynamoDBHashKeyAnnotation = field.getAnnotation(DynamoDBIndexHashKey.class);
109 
110 			if (dynamoDBRangeKeyAnnotation != null) {
111 				addGlobalSecondaryIndexNames(field, dynamoDBRangeKeyAnnotation);
112 			}
113 			if (dynamoDBHashKeyAnnotation != null) {
114 				addGlobalSecondaryIndexNames(field, dynamoDBHashKeyAnnotation);
115 			}
116 		});
117 		Assert.notNull(hashKeyPropertyName, "Unable to find hash key field or getter method on " + domainType + "!");
118 	}
119 
120 	public DynamoDBEntityInformation<T, ID> getEntityInformation() {
121 
122 		if (hasRangeKey) {
123 			DynamoDBHashAndRangeKeyExtractingEntityMetadataImpl<T, ID> metadata = new DynamoDBHashAndRangeKeyExtractingEntityMetadataImpl<T, ID>(
124 					domainType);
125 			return new DynamoDBIdIsHashAndRangeKeyEntityInformationImpl<>(domainType, metadata);
126 		} else {
127 			return new DynamoDBIdIsHashKeyEntityInformationImpl<>(domainType, this);
128 		}
129 	}
130 
131 	/*
132 	 * (non-Javadoc)
133 	 *
134 	 * @see org.springframework.data.repository.core.EntityMetadata#getJavaType()
135 	 */
136 	@Override
137 	public Class<T> getJavaType() {
138 		return domainType;
139 	}
140 
141 	@Override
142 	public boolean isHashKeyProperty(String propertyName) {
143 		return hashKeyPropertyName.equals(propertyName);
144 	}
145 
146 	protected boolean isFieldAnnotatedWith(final String propertyName, final Class<? extends Annotation> annotation) {
147 
148 		Field field = findField(propertyName);
149 		return field != null && field.getAnnotation(annotation) != null;
150 	}
151 
152 	private String toGetMethodName(String propertyName) {
153 		String methodName = propertyName.substring(0, 1).toUpperCase();
154 		if (propertyName.length() > 1) {
155 			methodName = methodName + propertyName.substring(1);
156 		}
157 		return "get" + methodName;
158 	}
159 
160 	protected String toSetterMethodNameFromAccessorMethod(Method method) {
161 		String accessorMethodName = method.getName();
162 		if (accessorMethodName.startsWith("get")) {
163 			return "set" + accessorMethodName.substring(3);
164 		} else if (accessorMethodName.startsWith("is")) {
165 			return "is" + accessorMethodName.substring(2);
166 		}
167 		return null;
168 	}
169 
170 	private String toIsMethodName(String propertyName) {
171 		String methodName = propertyName.substring(0, 1).toUpperCase();
172 		if (propertyName.length() > 1) {
173 			methodName = methodName + propertyName.substring(1);
174 		}
175 		return "is" + methodName;
176 	}
177 
178 	private Method findMethod(String propertyName) {
179 		Method method = ReflectionUtils.findMethod(domainType, toGetMethodName(propertyName));
180 		if (method == null) {
181 			method = ReflectionUtils.findMethod(domainType, toIsMethodName(propertyName));
182 		}
183 		return method;
184 
185 	}
186 
187 	private Field findField(String propertyName) {
188 		return ReflectionUtils.findField(domainType, propertyName);
189 	}
190 
191 	public String getOverriddenAttributeName(Method method) {
192 
193 		if (method != null) {
194 			if (method.getAnnotation(DynamoDBAttribute.class) != null
195 					&& !StringUtils.isEmpty(method.getAnnotation(DynamoDBAttribute.class).attributeName())) {
196 				return method.getAnnotation(DynamoDBAttribute.class).attributeName();
197 			}
198 			if (method.getAnnotation(DynamoDBHashKey.class) != null
199 					&& !StringUtils.isEmpty(method.getAnnotation(DynamoDBHashKey.class).attributeName())) {
200 				return method.getAnnotation(DynamoDBHashKey.class).attributeName();
201 			}
202 			if (method.getAnnotation(DynamoDBRangeKey.class) != null
203 					&& !StringUtils.isEmpty(method.getAnnotation(DynamoDBRangeKey.class).attributeName())) {
204 				return method.getAnnotation(DynamoDBRangeKey.class).attributeName();
205 			}
206 			if (method.getAnnotation(DynamoDBIndexRangeKey.class) != null
207 					&& !StringUtils.isEmpty(method.getAnnotation(DynamoDBIndexRangeKey.class).attributeName())) {
208 				return method.getAnnotation(DynamoDBIndexRangeKey.class).attributeName();
209 			}
210 			if (method.getAnnotation(DynamoDBIndexHashKey.class) != null
211 					&& !StringUtils.isEmpty(method.getAnnotation(DynamoDBIndexHashKey.class).attributeName())) {
212 				return method.getAnnotation(DynamoDBIndexHashKey.class).attributeName();
213 			}
214 			if (method.getAnnotation(DynamoDBVersionAttribute.class) != null
215 					&& !StringUtils.isEmpty(method.getAnnotation(DynamoDBVersionAttribute.class).attributeName())) {
216 				return method.getAnnotation(DynamoDBVersionAttribute.class).attributeName();
217 			}
218 		}
219 		return null;
220 
221 	}
222 
223 	@Override
224 	public Optional<String> getOverriddenAttributeName(final String propertyName) {
225 
226 		Method method = findMethod(propertyName);
227 		if (method != null) {
228 			if (method.getAnnotation(DynamoDBAttribute.class) != null
229 					&& !StringUtils.isEmpty(method.getAnnotation(DynamoDBAttribute.class).attributeName())) {
230 				return Optional.of(method.getAnnotation(DynamoDBAttribute.class).attributeName());
231 			}
232 			if (method.getAnnotation(DynamoDBHashKey.class) != null
233 					&& !StringUtils.isEmpty(method.getAnnotation(DynamoDBHashKey.class).attributeName())) {
234 				return Optional.of(method.getAnnotation(DynamoDBHashKey.class).attributeName());
235 			}
236 			if (method.getAnnotation(DynamoDBRangeKey.class) != null
237 					&& !StringUtils.isEmpty(method.getAnnotation(DynamoDBRangeKey.class).attributeName())) {
238 				return Optional.of(method.getAnnotation(DynamoDBRangeKey.class).attributeName());
239 			}
240 			if (method.getAnnotation(DynamoDBIndexRangeKey.class) != null
241 					&& !StringUtils.isEmpty(method.getAnnotation(DynamoDBIndexRangeKey.class).attributeName())) {
242 				return Optional.of(method.getAnnotation(DynamoDBIndexRangeKey.class).attributeName());
243 			}
244 			if (method.getAnnotation(DynamoDBIndexHashKey.class) != null
245 					&& !StringUtils.isEmpty(method.getAnnotation(DynamoDBIndexHashKey.class).attributeName())) {
246 				return Optional.of(method.getAnnotation(DynamoDBIndexHashKey.class).attributeName());
247 			}
248 			if (method.getAnnotation(DynamoDBVersionAttribute.class) != null
249 					&& !StringUtils.isEmpty(method.getAnnotation(DynamoDBVersionAttribute.class).attributeName())) {
250 				return Optional.of(method.getAnnotation(DynamoDBVersionAttribute.class).attributeName());
251 			}
252 		}
253 
254 		Field field = findField(propertyName);
255 		if (field != null) {
256 			if (field.getAnnotation(DynamoDBAttribute.class) != null
257 					&& !StringUtils.isEmpty(field.getAnnotation(DynamoDBAttribute.class).attributeName())) {
258 				return Optional.of(field.getAnnotation(DynamoDBAttribute.class).attributeName());
259 			}
260 			if (field.getAnnotation(DynamoDBHashKey.class) != null
261 					&& !StringUtils.isEmpty(field.getAnnotation(DynamoDBHashKey.class).attributeName())) {
262 				return Optional.of(field.getAnnotation(DynamoDBHashKey.class).attributeName());
263 			}
264 			if (field.getAnnotation(DynamoDBRangeKey.class) != null
265 					&& !StringUtils.isEmpty(field.getAnnotation(DynamoDBRangeKey.class).attributeName())) {
266 				return Optional.of(field.getAnnotation(DynamoDBRangeKey.class).attributeName());
267 			}
268 			if (field.getAnnotation(DynamoDBIndexRangeKey.class) != null
269 					&& !StringUtils.isEmpty(field.getAnnotation(DynamoDBIndexRangeKey.class).attributeName())) {
270 				return Optional.of(field.getAnnotation(DynamoDBIndexRangeKey.class).attributeName());
271 			}
272 			if (field.getAnnotation(DynamoDBIndexHashKey.class) != null
273 					&& !StringUtils.isEmpty(field.getAnnotation(DynamoDBIndexHashKey.class).attributeName())) {
274 				return Optional.of(field.getAnnotation(DynamoDBIndexHashKey.class).attributeName());
275 			}
276 			if (field.getAnnotation(DynamoDBVersionAttribute.class) != null
277 					&& !StringUtils.isEmpty(field.getAnnotation(DynamoDBVersionAttribute.class).attributeName())) {
278 				return Optional.of(field.getAnnotation(DynamoDBVersionAttribute.class).attributeName());
279 			}
280 		}
281 		return Optional.empty();
282 
283 	}
284 
285 	@Override
286 	@SuppressWarnings("deprecation")
287 	public <V extends DynamoDBMarshaller<?>> V getMarshallerForProperty(final String propertyName) {
288 		// TODO #28
289 		DynamoDBMarshalling annotation = null;
290 
291 		Method method = findMethod(propertyName);
292 		if (method != null) {
293 			annotation = method.getAnnotation(DynamoDBMarshalling.class);
294 		}
295 
296 		if (annotation == null) {
297 			Field field = findField(propertyName);
298 			if (field != null) {
299 				annotation = field.getAnnotation(DynamoDBMarshalling.class);
300 			}
301 		}
302 
303 		if (annotation != null) {
304 			try {
305 				@SuppressWarnings("unchecked")
306 				Class<V> marshallerClazz = (Class<V>) annotation.marshallerClass();
307 				return marshallerClazz.getDeclaredConstructor().newInstance();
308 			} catch (InstantiationException | IllegalAccessException | NoSuchMethodException
309 					| InvocationTargetException e) {
310 				throw new RuntimeException(e);
311 			}
312 		}
313 
314 		return null;
315 	}
316 
317 	@Override
318 	public DynamoDBTypeConverter<?, ?> getTypeConverterForProperty(final String propertyName) {
319 		DynamoDBTypeConverted annotation = null;
320 
321 		Method method = findMethod(propertyName);
322 		if (method != null) {
323 			annotation = method.getAnnotation(DynamoDBTypeConverted.class);
324 		}
325 
326 		if (annotation == null) {
327 			Field field = findField(propertyName);
328 			if (field != null) {
329 				annotation = field.getAnnotation(DynamoDBTypeConverted.class);
330 			}
331 		}
332 
333 		if (annotation != null) {
334 			try {
335 				return annotation.converter().getDeclaredConstructor().newInstance();
336 			} catch (InstantiationException | IllegalAccessException | IllegalArgumentException
337 					| InvocationTargetException | NoSuchMethodException | SecurityException e) {
338 				throw new RuntimeException(e);
339 			}
340 		}
341 
342 		return null;
343 	}
344 
345 	protected String getPropertyNameForAccessorMethod(Method method) {
346 		String methodName = method.getName();
347 		String propertyName = null;
348 		if (methodName.startsWith("get")) {
349 			propertyName = methodName.substring(3);
350 		} else if (methodName.startsWith("is")) {
351 			propertyName = methodName.substring(2);
352 		}
353 		Assert.notNull(propertyName, "Hash or range key annotated accessor methods must start with 'get' or 'is'");
354 
355 		String firstLetter = propertyName.substring(0, 1);
356 		String remainder = propertyName.substring(1);
357 		return firstLetter.toLowerCase() + remainder;
358 	}
359 
360 	protected String getPropertyNameForField(Field field) {
361 		return field.getName();
362 	}
363 
364 	@Override
365 	public String getHashKeyPropertyName() {
366 		return hashKeyPropertyName;
367 	}
368 
369 	private void addGlobalSecondaryIndexNames(Method method, DynamoDBIndexRangeKey dynamoDBIndexRangeKey) {
370 
371 		if (dynamoDBIndexRangeKey.globalSecondaryIndexNames() != null
372 				&& dynamoDBIndexRangeKey.globalSecondaryIndexNames().length > 0) {
373 			String propertyName = getPropertyNameForAccessorMethod(method);
374 
375 			globalSecondaryIndexNames.put(propertyName,
376 					method.getAnnotation(DynamoDBIndexRangeKey.class).globalSecondaryIndexNames());
377 			globalIndexRangeKeyPropertyNames.add(propertyName);
378 
379 		}
380 		if (dynamoDBIndexRangeKey.globalSecondaryIndexName() != null
381 				&& dynamoDBIndexRangeKey.globalSecondaryIndexName().trim().length() > 0) {
382 			String propertyName = getPropertyNameForAccessorMethod(method);
383 			globalSecondaryIndexNames.put(propertyName,
384 					new String[]{method.getAnnotation(DynamoDBIndexRangeKey.class).globalSecondaryIndexName()});
385 			globalIndexRangeKeyPropertyNames.add(propertyName);
386 
387 		}
388 
389 	}
390 
391 	private void addGlobalSecondaryIndexNames(Field field, DynamoDBIndexRangeKey dynamoDBIndexRangeKey) {
392 
393 		if (dynamoDBIndexRangeKey.globalSecondaryIndexNames() != null
394 				&& dynamoDBIndexRangeKey.globalSecondaryIndexNames().length > 0) {
395 			String propertyName = getPropertyNameForField(field);
396 
397 			globalSecondaryIndexNames.put(propertyName,
398 					field.getAnnotation(DynamoDBIndexRangeKey.class).globalSecondaryIndexNames());
399 			globalIndexRangeKeyPropertyNames.add(propertyName);
400 
401 		}
402 		if (dynamoDBIndexRangeKey.globalSecondaryIndexName() != null
403 				&& dynamoDBIndexRangeKey.globalSecondaryIndexName().trim().length() > 0) {
404 			String propertyName = getPropertyNameForField(field);
405 			globalSecondaryIndexNames.put(propertyName,
406 					new String[]{field.getAnnotation(DynamoDBIndexRangeKey.class).globalSecondaryIndexName()});
407 			globalIndexRangeKeyPropertyNames.add(propertyName);
408 
409 		}
410 
411 	}
412 
413 	private void addGlobalSecondaryIndexNames(Method method, DynamoDBIndexHashKey dynamoDBIndexHashKey) {
414 
415 		if (dynamoDBIndexHashKey.globalSecondaryIndexNames() != null
416 				&& dynamoDBIndexHashKey.globalSecondaryIndexNames().length > 0) {
417 			String propertyName = getPropertyNameForAccessorMethod(method);
418 
419 			globalSecondaryIndexNames.put(propertyName,
420 					method.getAnnotation(DynamoDBIndexHashKey.class).globalSecondaryIndexNames());
421 			globalIndexHashKeyPropertyNames.add(propertyName);
422 
423 		}
424 		if (dynamoDBIndexHashKey.globalSecondaryIndexName() != null
425 				&& dynamoDBIndexHashKey.globalSecondaryIndexName().trim().length() > 0) {
426 			String propertyName = getPropertyNameForAccessorMethod(method);
427 
428 			globalSecondaryIndexNames.put(propertyName,
429 					new String[]{method.getAnnotation(DynamoDBIndexHashKey.class).globalSecondaryIndexName()});
430 			globalIndexHashKeyPropertyNames.add(propertyName);
431 
432 		}
433 	}
434 
435 	private void addGlobalSecondaryIndexNames(Field field, DynamoDBIndexHashKey dynamoDBIndexHashKey) {
436 
437 		if (dynamoDBIndexHashKey.globalSecondaryIndexNames() != null
438 				&& dynamoDBIndexHashKey.globalSecondaryIndexNames().length > 0) {
439 			String propertyName = getPropertyNameForField(field);
440 
441 			globalSecondaryIndexNames.put(propertyName,
442 					field.getAnnotation(DynamoDBIndexHashKey.class).globalSecondaryIndexNames());
443 			globalIndexHashKeyPropertyNames.add(propertyName);
444 
445 		}
446 		if (dynamoDBIndexHashKey.globalSecondaryIndexName() != null
447 				&& dynamoDBIndexHashKey.globalSecondaryIndexName().trim().length() > 0) {
448 			String propertyName = getPropertyNameForField(field);
449 
450 			globalSecondaryIndexNames.put(propertyName,
451 					new String[]{field.getAnnotation(DynamoDBIndexHashKey.class).globalSecondaryIndexName()});
452 			globalIndexHashKeyPropertyNames.add(propertyName);
453 
454 		}
455 	}
456 
457 	@Override
458 	public Map<String, String[]> getGlobalSecondaryIndexNamesByPropertyName() {
459 		return globalSecondaryIndexNames;
460 	}
461 
462 	@Override
463 	public boolean isGlobalIndexHashKeyProperty(String propertyName) {
464 		return globalIndexHashKeyPropertyNames.contains(propertyName);
465 	}
466 
467 	@Override
468 	public boolean isGlobalIndexRangeKeyProperty(String propertyName) {
469 		return globalIndexRangeKeyPropertyNames.contains(propertyName);
470 	}
471 
472 }