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.core;
17  
18  import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
19  import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper;
20  import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper.FailedBatch;
21  import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperConfig;
22  import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperTableModel;
23  import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBQueryExpression;
24  import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBScanExpression;
25  import com.amazonaws.services.dynamodbv2.datamodeling.KeyPair;
26  import com.amazonaws.services.dynamodbv2.datamodeling.PaginatedQueryList;
27  import com.amazonaws.services.dynamodbv2.datamodeling.PaginatedScanList;
28  import com.amazonaws.services.dynamodbv2.model.QueryRequest;
29  import com.amazonaws.services.dynamodbv2.model.QueryResult;
30  import com.amazonaws.services.dynamodbv2.model.Select;
31  import org.slf4j.Logger;
32  import org.slf4j.LoggerFactory;
33  import org.socialsignin.spring.data.dynamodb.mapping.event.AfterDeleteEvent;
34  import org.socialsignin.spring.data.dynamodb.mapping.event.AfterLoadEvent;
35  import org.socialsignin.spring.data.dynamodb.mapping.event.AfterQueryEvent;
36  import org.socialsignin.spring.data.dynamodb.mapping.event.AfterSaveEvent;
37  import org.socialsignin.spring.data.dynamodb.mapping.event.AfterScanEvent;
38  import org.socialsignin.spring.data.dynamodb.mapping.event.BeforeDeleteEvent;
39  import org.socialsignin.spring.data.dynamodb.mapping.event.BeforeSaveEvent;
40  import org.socialsignin.spring.data.dynamodb.mapping.event.DynamoDBMappingEvent;
41  import org.springframework.beans.BeansException;
42  import org.springframework.context.ApplicationContext;
43  import org.springframework.context.ApplicationContextAware;
44  import org.springframework.context.ApplicationEventPublisher;
45  import org.springframework.lang.Nullable;
46  import org.springframework.util.Assert;
47  
48  import java.util.List;
49  import java.util.Map;
50  import java.util.function.Function;
51  import java.util.stream.Collectors;
52  
53  public class DynamoDBTemplate implements DynamoDBOperations, ApplicationContextAware {
54  	private static final Logger LOGGER = LoggerFactory.getLogger(DynamoDBTemplate.class);
55  	private final DynamoDBMapper dynamoDBMapper;
56  	private final AmazonDynamoDB amazonDynamoDB;
57  	private final DynamoDBMapperConfig dynamoDBMapperConfig;
58  	private ApplicationEventPublisher eventPublisher;
59  
60  	/**
61  	 * Convenient constructor to use the default
62  	 * {@link DynamoDBMapper#DynamoDBMapper(AmazonDynamoDB)}
63  	 * 
64  	 * @param amazonDynamoDB
65  	 *            The AWS SDK instance to talk to DynamoDB
66  	 * @param dynamoDBMapperConfig
67  	 *            The config to use
68  	 */
69  	public DynamoDBTemplate(AmazonDynamoDB amazonDynamoDB, DynamoDBMapperConfig dynamoDBMapperConfig) {
70  		this(amazonDynamoDB, dynamoDBMapperConfig, null);
71  	}
72  
73  	/**
74  	 * Convenient constructor to use the {@link DynamoDBMapperConfig#DEFAULT}
75  	 * 
76  	 * @param amazonDynamoDB
77  	 *            The AWS SDK instance to talk to DynamoDB
78  	 * @param dynamoDBMapper
79  	 *            The Mapper to use
80  	 */
81  	public DynamoDBTemplate(AmazonDynamoDB amazonDynamoDB, DynamoDBMapper dynamoDBMapper) {
82  		this(amazonDynamoDB, null, dynamoDBMapper);
83  	}
84  
85  	/**
86  	 * Convenient construcotr to thse the {@link DynamoDBMapperConfig#DEFAULT} and
87  	 * default {@link DynamoDBMapper#DynamoDBMapper(AmazonDynamoDB)}
88  	 * 
89  	 * @param amazonDynamoDB
90  	 *            The AWS SDK instance to talk to DynamoDB
91  	 */
92  	public DynamoDBTemplate(AmazonDynamoDB amazonDynamoDB) {
93  		this(amazonDynamoDB, null, null);
94  	}
95  
96  	/**
97  	 * Initializes a new {@code DynamoDBTemplate}. The following combinations are
98  	 * valid:
99  	 * 
100 	 * @param amazonDynamoDB
101 	 *            must not be {@code null}
102 	 * @param dynamoDBMapperConfig
103 	 *            can be {@code null} - {@link DynamoDBMapperConfig#DEFAULT} is used
104 	 *            if {@code null} is passed in
105 	 * @param dynamoDBMapper
106 	 *            can be {@code null} -
107 	 *            {@link DynamoDBMapper#DynamoDBMapper(AmazonDynamoDB, DynamoDBMapperConfig)}
108 	 *            is used if {@code null} is passed in
109 	 */
110 	public DynamoDBTemplate(AmazonDynamoDB amazonDynamoDB, DynamoDBMapperConfig dynamoDBMapperConfig,
111 			DynamoDBMapper dynamoDBMapper) {
112 		Assert.notNull(amazonDynamoDB, "amazonDynamoDB must not be null!");
113 		this.amazonDynamoDB = amazonDynamoDB;
114 
115 		if (dynamoDBMapperConfig == null) {
116 			this.dynamoDBMapperConfig = DynamoDBMapperConfig.DEFAULT;
117 		} else {
118 
119 			// #146, #81 #157
120 			// Trying to fix half-initialized DynamoDBMapperConfigs here.
121 			// The old documentation advised to start with an empty builder. Therefore we
122 			// try here to set required fields to their defaults -
123 			// As the documentation at
124 			// https://github.com/derjust/spring-data-dynamodb/wiki/Alter-table-name-during-runtime
125 			// (same as https://git.io/DynamoDBMapperConfig)
126 			// now does: Start with #DEFAULT and add what's required
127 			DynamoDBMapperConfig.Builder emptyBuilder = DynamoDBMapperConfig.builder(); // empty (!) builder
128 
129 			if (dynamoDBMapperConfig.getConversionSchema() == null) {
130 				LOGGER.warn(
131 						"No ConversionSchema set in the provided dynamoDBMapperConfig! Merging with DynamoDBMapperConfig.DEFAULT - Please see https://git.io/DynamoDBMapperConfig");
132 				// DynamoDBMapperConfig#DEFAULT comes with a ConversionSchema
133 				emptyBuilder.withConversionSchema(DynamoDBMapperConfig.DEFAULT.getConversionSchema());
134 			}
135 
136 			if (dynamoDBMapperConfig.getTypeConverterFactory() == null) {
137 				LOGGER.warn(
138 						"No TypeConverterFactory set in the provided dynamoDBMapperConfig! Merging with DynamoDBMapperConfig.DEFAULT - Please see https://git.io/DynamoDBMapperConfig");
139 				// DynamoDBMapperConfig#DEFAULT comes with a TypeConverterFactory
140 				emptyBuilder.withTypeConverterFactory(DynamoDBMapperConfig.DEFAULT.getTypeConverterFactory());
141 			}
142 
143 			// Deprecated but the only way how DynamoDBMapperConfig#merge is exposed
144 			this.dynamoDBMapperConfig = new DynamoDBMapperConfig(dynamoDBMapperConfig, emptyBuilder.build());
145 		}
146 
147 		if (dynamoDBMapper == null) {
148 			this.dynamoDBMapper = new DynamoDBMapper(amazonDynamoDB, dynamoDBMapperConfig);
149 		} else {
150 			this.dynamoDBMapper = dynamoDBMapper;
151 		}
152 	}
153 
154 	@Override
155 	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
156 		this.eventPublisher = applicationContext;
157 	}
158 
159 	@Override
160 	public <T> int count(Class<T> domainClass, DynamoDBQueryExpression<T> queryExpression) {
161 		return dynamoDBMapper.count(domainClass, queryExpression);
162 	}
163 
164 	@Override
165 	public <T> PaginatedQueryList<T> query(Class<T> domainClass, DynamoDBQueryExpression<T> queryExpression) {
166 		PaginatedQueryList<T> results = dynamoDBMapper.query(domainClass, queryExpression);
167 		maybeEmitEvent(results, AfterQueryEvent::new);
168 		return results;
169 	}
170 
171 	@Override
172 	public <T> int count(Class<T> domainClass, DynamoDBScanExpression scanExpression) {
173 		return dynamoDBMapper.count(domainClass, scanExpression);
174 	}
175 
176 	@Override
177 	public <T> T load(Class<T> domainClass, Object hashKey, Object rangeKey) {
178 		T entity = dynamoDBMapper.load(domainClass, hashKey, rangeKey);
179 		maybeEmitEvent(entity, AfterLoadEvent::new);
180 
181 		return entity;
182 	}
183 
184 	@Override
185 	public <T> T load(Class<T> domainClass, Object hashKey) {
186 		T entity = dynamoDBMapper.load(domainClass, hashKey);
187 		maybeEmitEvent(entity, AfterLoadEvent::new);
188 
189 		return entity;
190 	}
191 
192 	@Override
193 	public <T> PaginatedScanList<T> scan(Class<T> domainClass, DynamoDBScanExpression scanExpression) {
194 		PaginatedScanList<T> results = dynamoDBMapper.scan(domainClass, scanExpression);
195 		maybeEmitEvent(results, AfterScanEvent::new);
196 		return results;
197 	}
198 
199 	@SuppressWarnings("unchecked")
200 	@Override
201 	public <T> List<T> batchLoad(Map<Class<?>, List<KeyPair>> itemsToGet) {
202 		return dynamoDBMapper.batchLoad(itemsToGet).values().stream().flatMap(v -> v.stream()).map(e -> (T) e)
203 				.map(entity -> {
204 					maybeEmitEvent(entity, AfterLoadEvent::new);
205 					return entity;
206 				}).collect(Collectors.toList());
207 	}
208 
209 	@Override
210 	public <T> T save(T entity) {
211 		maybeEmitEvent(entity, BeforeSaveEvent::new);
212 		dynamoDBMapper.save(entity);
213 		maybeEmitEvent(entity, AfterSaveEvent::new);
214 		return entity;
215 
216 	}
217 
218 	@Override
219 	public List<FailedBatch> batchSave(Iterable<?> entities) {
220 		entities.forEach(it -> maybeEmitEvent(it, BeforeSaveEvent::new));
221 
222 		List<FailedBatch> result = dynamoDBMapper.batchSave(entities);
223 
224 		entities.forEach(it -> maybeEmitEvent(it, AfterSaveEvent::new));
225 		return result;
226 	}
227 
228 	@Override
229 	public <T> T delete(T entity) {
230 		maybeEmitEvent(entity, BeforeDeleteEvent::new);
231 		dynamoDBMapper.delete(entity);
232 		maybeEmitEvent(entity, AfterDeleteEvent::new);
233 		return entity;
234 	}
235 
236 	@Override
237 	public List<FailedBatch> batchDelete(Iterable<?> entities) {
238 		entities.forEach(it -> maybeEmitEvent(it, BeforeDeleteEvent::new));
239 
240 		List<FailedBatch> result = dynamoDBMapper.batchDelete(entities);
241 
242 		entities.forEach(it -> maybeEmitEvent(it, AfterDeleteEvent::new));
243 		return result;
244 	}
245 
246 	@Override
247 	public <T> PaginatedQueryList<T> query(Class<T> clazz, QueryRequest queryRequest) {
248 		QueryResult queryResult = amazonDynamoDB.query(queryRequest);
249 		return new PaginatedQueryList<T>(dynamoDBMapper, clazz, amazonDynamoDB, queryRequest, queryResult,
250 				dynamoDBMapperConfig.getPaginationLoadingStrategy(), dynamoDBMapperConfig);
251 	}
252 
253 	@Override
254 	public <T> int count(Class<T> clazz, QueryRequest mutableQueryRequest) {
255 		mutableQueryRequest.setSelect(Select.COUNT);
256 
257 		// Count queries can also be truncated for large datasets
258 		int count = 0;
259 		QueryResult queryResult = null;
260 		do {
261 			queryResult = amazonDynamoDB.query(mutableQueryRequest);
262 			count += queryResult.getCount();
263 			mutableQueryRequest.setExclusiveStartKey(queryResult.getLastEvaluatedKey());
264 		} while (queryResult.getLastEvaluatedKey() != null);
265 
266 		return count;
267 	}
268 
269 	@Override
270 	public <T> String getOverriddenTableName(Class<T> domainClass, String tableName) {
271 		if (dynamoDBMapperConfig.getTableNameOverride() != null) {
272 			if (dynamoDBMapperConfig.getTableNameOverride().getTableName() != null) {
273 				tableName = dynamoDBMapperConfig.getTableNameOverride().getTableName();
274 			} else {
275 				tableName = dynamoDBMapperConfig.getTableNameOverride().getTableNamePrefix() + tableName;
276 			}
277 		} else if (dynamoDBMapperConfig.getTableNameResolver() != null) {
278 			tableName = dynamoDBMapperConfig.getTableNameResolver().getTableName(domainClass, dynamoDBMapperConfig);
279 		}
280 
281 		return tableName;
282 	}
283 
284 	/**
285 	 * {@inheritDoc}
286 	 */
287 	@Override
288 	public <T> DynamoDBMapperTableModel<T> getTableModel(Class<T> domainClass) {
289 		return dynamoDBMapper.getTableModel(domainClass, dynamoDBMapperConfig);
290 	}
291 
292 	protected <T> void maybeEmitEvent(@Nullable T source, Function<T, DynamoDBMappingEvent<T>> factory) {
293 		if (eventPublisher != null) {
294 			if (source != null) {
295 				DynamoDBMappingEvent<T> event = factory.apply(source);
296 
297 				eventPublisher.publishEvent(event);
298 			}
299 		}
300 
301 	}
302 }