1 package edu.memphis.iis.demosurvey;
2
3 import java.lang.annotation.Annotation;
4 import java.lang.reflect.Method;
5 import java.util.HashMap;
6 import java.util.List;
7 import java.util.Map;
8
9 import org.slf4j.Logger;
10 import org.slf4j.LoggerFactory;
11
12 import com.amazonaws.auth.DefaultAWSCredentialsProviderChain;
13 import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient;
14 import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBHashKey;
15 import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper;
16 import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperConfig;
17 import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBSaveExpression;
18 import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBScanExpression;
19 import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTable;
20 import com.amazonaws.services.dynamodbv2.document.DynamoDB;
21 import com.amazonaws.services.dynamodbv2.document.Table;
22 import com.amazonaws.services.dynamodbv2.model.ExpectedAttributeValue;
23 import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput;
24
25
26 /**
27 * This is a very simple demonstration of an interface to a data store,
28 * sometimes called a DAO. Depending on what data you're working with,
29 * if you're using something like Spring or Guice, and how large your
30 * project is, you might have much more complicated DAO's, you might
31 * skip them entirely, or they might be provided by a library.
32 *
33 * Note that we manually create our DynamoDB client, db, and mapper
34 * instances in the constructor. In a large system, we would want to
35 * use an Inversion of Control (aka Dependency Injection) pattern.
36 * Those instances would be injected by some kind of context object that
37 * could vary for unit tests, local workstation debugging, and running
38 * in the actual AWS cloud
39 *
40 * For this simple demo, we are just using some custom logic based on
41 * system properties. See the pom.xml for how we set aws.dynamoEndpoint
42 * for local Tomcat testing AND the AWS credential properties so that
43 * our use of DefaultAWSCredentialsProviderChain works.
44 *
45 * Note that this class supports auto-creation of all tables listed in
46 * the TABLES variable. It also supports automatic key checking (which
47 * we use in saveSurvey).
48 */
49 public class DataStoreClient {
50 private final static Logger logger = LoggerFactory.getLogger(DataStoreClient.class);
51
52 /**
53 * The list of tables that should be insured (created if missing) on
54 * startup. Note that if there is a class in this array that isn't
55 * annotated with @DynamoDBTable with tableName specified you WILL
56 * get exceptions on startup).
57 * */
58 private final static Class<?>[] TABLES = {Survey.class};
59
60 /** Default read capacity set for DynamoDB tables on creation */
61 private final static long DEFAULT_READ_CAPACITY = 2L;
62
63 /** Default write capacity set for DynamoDB tables on creation */
64 private final static long DEFAULT_WRITE_CAPACITY = 5L;
65
66 /** Created and managed by constructor */
67 private AmazonDynamoDBClient client;
68
69 /** Created and managed by helper function - shouldn't be used directly in code */
70 private DynamoDB db;
71
72 /** Created and managed by helper function - shouldn't be used directly in code */
73 private DynamoDBMapper mapper;
74
75 /** Default constructor */
76 public DataStoreClient() {
77 // Note our lack of credentials - this is because in Elastic Beanstalk
78 // we will be specifying AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
79 // via environment variables
80 client = new AmazonDynamoDBClient(new DefaultAWSCredentialsProviderChain());
81
82 String endpoint = System.getProperty("aws.dynamoEndpoint");
83 if (!Utils.isBlankString(endpoint)) {
84 //No env vars for security - we must be in testing
85 logger.info("Using specified endpoint {}", endpoint);
86 client.setEndpoint(endpoint);
87 }
88
89 //These will be lazy-init'ed by our helper functions
90 db = null;
91 mapper = null;
92 }
93
94 /**
95 * Simple helper to lazy-create the (low-level) Dynamo DB instance for
96 * our current client.
97 * @return valid instance of Dynamo DB
98 */
99 protected DynamoDB getDB() {
100 if (db == null) {
101 db = new DynamoDB(client);
102 }
103 return db;
104 }
105
106 /**
107 * Simple helper to lazy-create the (high-level) Dynamo DB mapper instance
108 * for our current client.
109 * @return valid instance of Dynamo DB Mapper
110 */
111 protected DynamoDBMapper getMapper() {
112 if (mapper == null) {
113 mapper = new DynamoDBMapper(client);
114 }
115 return mapper;
116 }
117
118 /**
119 * Insure that the current database schema is properly loaded.
120 * This example pattern creates a set of tables needed, removes
121 * any previously created tables from that set, and then creates
122 * any tables left over.
123 */
124 public void ensureSchema() {
125 Map<String, Class<?>> tablesNeeded = new HashMap<>();
126
127 //Extract table names from table classes
128 for(Class<?> c: TABLES) {
129 DynamoDBTable ann = (DynamoDBTable)c.getAnnotation(DynamoDBTable.class);
130 tablesNeeded.put(ann.tableName(), c);
131 logger.info("Table Configuration Found: " + ann.tableName());
132 }
133
134 for(Table t: getDB().listTables()) {
135 String tableName = t.getTableName();
136 logger.info("Existing Table Found: " + tableName);
137 tablesNeeded.remove(tableName);
138 }
139
140 //Create any tables we didn't find
141 //NOTE that we iterate through the keys and then access the value in
142 //the loop below. It would be more efficient to iterate over the
143 //EntrySet (which would contain both the key and the value). However,
144 //we leaving it this way so that there will be a warning to display in
145 //the FindBug's report.
146 for(String tableName: tablesNeeded.keySet()) {
147 logger.info("CREATING Table: " + tableName);
148
149 //FIND BUGS: this line should be flagged in the FindBugs report
150 Class<?> tableClass = tablesNeeded.get(tableName);
151
152 db.createTable(getMapper()
153 .generateCreateTableRequest(tableClass)
154 .withProvisionedThroughput(new ProvisionedThroughput()
155 .withReadCapacityUnits(DEFAULT_READ_CAPACITY)
156 .withWriteCapacityUnits(DEFAULT_WRITE_CAPACITY)
157 )
158 );
159 }
160 }
161
162 /**
163 * Persist the given survey
164 *
165 * @param survey the object to save
166 * @param allowOverwrite if true, a previous record will overwritten
167 */
168 public void saveSurvey(Survey survey, boolean allowOverwrite) {
169 if (survey == null || !survey.isValid()) {
170 throw new IllegalArgumentException("Invalid survey cannot be saved");
171 }
172
173 if (allowOverwrite) {
174 //Just fire a save and completely overwrite the original record
175 getMapper().save(
176 survey,
177 new DynamoDBMapperConfig(DynamoDBMapperConfig.SaveBehavior.CLOBBER)
178 );
179 }
180 else {
181 //Throw an exception if the record already exists
182 getMapper().save(
183 survey,
184 new DynamoDBSaveExpression()
185 .withExpected(expectKey(Survey.class))
186 );
187 }
188 }
189
190 /**
191 * Return a list of all surveys. Note that the underlying implementation
192 * of the list is unspecified. Currently we use the AWS SDK's lazy-loading
193 * list (which returns a page at a time). There may be delays while iterating
194 * over this list, AND this may change in the future
195 *
196 * @return a List<> of Survey instances, or a List<> of size 0 if no
197 * Survey's are found
198 */
199 public List<Survey> findSurveys() {
200 return getMapper().scan(
201 Survey.class,
202 new DynamoDBScanExpression()
203 );
204 }
205
206 /**
207 * Given a "table" (the Class<?> for a class that is annotated with
208 * DynamoDBTable), return the appropriate map. NOTE that if you don't
209 * have a method annotated with DynamoDBHashKey and attributeName
210 * specified, an exception will be thrown
211 * @param table instance of Class<?>
212 * @return a Map suitable for use as an Expected in a DynamoDBSaveExpression
213 */
214 private Map<String, ExpectedAttributeValue> expectKey(Class<?> table) {
215 String keyName = keyAttributeName(table);
216 if (Utils.isBlankString(keyName)) {
217 throw new IllegalArgumentException(table.getCanonicalName() + " has no DynamoDBHashKey specified");
218 }
219
220 Map<String, ExpectedAttributeValue> expected = new HashMap<>();
221 expected.put(keyName, new ExpectedAttributeValue(false));
222 return expected;
223 }
224
225 /**
226 * Given a table (the Class<?> for a class that is annotated with
227 * DynamoDBTable), return the attribute name of the key to the table
228 * as specified by the DynamoDBHashKey annotation
229 * @param table instance of Class<?> for a class annotated DynamoDBTable
230 * @return the attribute name of the key for the table
231 */
232 private String keyAttributeName(Class<?> table) {
233 for(Method m: table.getMethods()) {
234 Annotation keyAttr = m.getAnnotation(DynamoDBHashKey.class);
235 if (keyAttr != null) {
236 return ((DynamoDBHashKey)keyAttr).attributeName();
237 }
238 }
239
240 return null; //Not found
241 }
242 }