View Javadoc

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 }