org.selman.tweetamo.PersistentStore.java Source code

Java tutorial

Introduction

Here is the source code for org.selman.tweetamo.PersistentStore.java

Source

package org.selman.tweetamo;

/*
 * Copyright 2013 Daniel Selman
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import twitter4j.Status;

import com.amazonaws.AmazonClientException;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.auth.ClasspathPropertiesFileCredentialsProvider;
import com.amazonaws.regions.Region;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient;
import com.amazonaws.services.dynamodbv2.model.AttributeDefinition;
import com.amazonaws.services.dynamodbv2.model.AttributeValue;
import com.amazonaws.services.dynamodbv2.model.ComparisonOperator;
import com.amazonaws.services.dynamodbv2.model.Condition;
import com.amazonaws.services.dynamodbv2.model.CreateTableRequest;
import com.amazonaws.services.dynamodbv2.model.DescribeTableRequest;
import com.amazonaws.services.dynamodbv2.model.GlobalSecondaryIndex;
import com.amazonaws.services.dynamodbv2.model.KeySchemaElement;
import com.amazonaws.services.dynamodbv2.model.KeyType;
import com.amazonaws.services.dynamodbv2.model.Projection;
import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput;
import com.amazonaws.services.dynamodbv2.model.PutItemRequest;
import com.amazonaws.services.dynamodbv2.model.QueryRequest;
import com.amazonaws.services.dynamodbv2.model.QueryResult;
import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType;
import com.amazonaws.services.dynamodbv2.model.ScanRequest;
import com.amazonaws.services.dynamodbv2.model.ScanResult;
import com.amazonaws.services.dynamodbv2.model.Select;
import com.amazonaws.services.dynamodbv2.model.TableDescription;
import com.amazonaws.services.dynamodbv2.model.TableStatus;

/**
 * Stores Twitter status updates in a DynamoDB table. Provides some
 * simple query APIs to retrieve status updates.
 */
public class PersistentStore {
    private static AmazonDynamoDBClient dynamoDB;
    private static final Log LOG = LogFactory.getLog(PersistentStore.class);

    private static final String TABLE_NAME = "tweetamo_status";
    public static final String COL_ID = "id";
    public static final String COL_CREATEDAT = "createdAt";
    public static final String COL_LAT = "lat";
    public static final String COL_LONG = "long";
    public static final String COL_SCREENNAME = "screenName";
    public static final String COL_TEXT = "text";

    public static final String INDEX_SCREENNAME = "INDEX_screenName";

    private static PersistentStore INSTANCE = null;

    /**
     * The only information needed to create a client are security credentials
     * consisting of the AWS Access Key ID and Secret Access Key. All other
     * configuration, such as the service endpoints, are performed
     * automatically. Client parameters, such as proxies, can be specified in an
     * optional ClientConfiguration object when constructing a client.
     * 
     * @see com.amazonaws.auth.BasicAWSCredentials
     * @see com.amazonaws.auth.PropertiesCredentials
     * @see com.amazonaws.ClientConfiguration
     */
    private PersistentStore(Region region, long readCapacity, long writeCapacity) throws Exception {
        /*
         * This credentials provider implementation loads your AWS credentials
         * from a properties file at the root of your classpath.
         */
        dynamoDB = new AmazonDynamoDBClient(new ClasspathPropertiesFileCredentialsProvider());
        dynamoDB.setRegion(region);

        try {
            if (!tablesExist()) {
                createTables(readCapacity, writeCapacity);
            }
            waitForTableToBecomeAvailable(TABLE_NAME);
        } catch (Exception e) {
            handleException(e);
        }
    }

    public static PersistentStore getInstance() {
        synchronized (PersistentStore.class) {
            if (INSTANCE == null) {
                try {
                    INSTANCE = new PersistentStore(Region.getRegion(Regions.US_EAST_1), 1L, 50L);
                } catch (Exception e) {
                    LOG.error("Failed to create PersistentStore", e);
                }
            }

            return INSTANCE;
        }
    }

    private void createTables(long readCapacity, long writeCapacity) throws Exception {
        // ID | createdAt | lat | long | screen name | text |
        // Primary index is by ID
        // Global Secondary index is by screen name + createdAt

        try {
            CreateTableRequest createTableRequest = new CreateTableRequest().withTableName(TABLE_NAME)
                    .withKeySchema(new KeySchemaElement().withAttributeName(COL_ID).withKeyType(KeyType.HASH))
                    .withAttributeDefinitions(
                            new AttributeDefinition().withAttributeName(COL_ID)
                                    .withAttributeType(ScalarAttributeType.N),
                            new AttributeDefinition().withAttributeName(COL_CREATEDAT)
                                    .withAttributeType(ScalarAttributeType.N),
                            new AttributeDefinition().withAttributeName(COL_SCREENNAME)
                                    .withAttributeType(ScalarAttributeType.S))
                    .withProvisionedThroughput(new ProvisionedThroughput().withReadCapacityUnits(readCapacity)
                            .withWriteCapacityUnits(writeCapacity))
                    .withGlobalSecondaryIndexes(new GlobalSecondaryIndex().withIndexName(INDEX_SCREENNAME)
                            .withProvisionedThroughput(new ProvisionedThroughput().withReadCapacityUnits((long) 10)
                                    .withWriteCapacityUnits((long) 1))
                            .withProjection(new Projection().withProjectionType("ALL")).withKeySchema(
                                    new KeySchemaElement().withAttributeName(COL_SCREENNAME)
                                            .withKeyType(KeyType.HASH),
                                    new KeySchemaElement().withAttributeName(COL_CREATEDAT)
                                            .withKeyType(KeyType.RANGE)));

            TableDescription createdTableDescription = dynamoDB.createTable(createTableRequest)
                    .getTableDescription();
            LOG.info("Created Table: " + createdTableDescription);
        } catch (Exception e) {
            handleException(e);
        }
    }

    private boolean tablesExist() {
        try {
            DescribeTableRequest describeTableRequest = new DescribeTableRequest().withTableName(TABLE_NAME);
            dynamoDB.describeTable(describeTableRequest).getTable();
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    public void add(Status status) throws Exception {
        try {
            Map<String, AttributeValue> item = newItem(status);
            PutItemRequest putItemRequest = new PutItemRequest(TABLE_NAME, item);
            dynamoDB.putItem(putItemRequest);
            LOG.info("Stored status in Dynamo: " + status.getId());
        } catch (Exception e) {
            handleException(e);
        }
    }

    public QueryResult getLatestTweetsForScreenName(String screenName, long timestamp) throws Exception {
        try {
            long startDateMilli = System.currentTimeMillis();

            Map<String, Condition> keyConditions = new HashMap<String, Condition>();

            keyConditions.put(COL_SCREENNAME, new Condition().withComparisonOperator(ComparisonOperator.EQ)
                    .withAttributeValueList(new AttributeValue().withS(screenName)));

            keyConditions.put(COL_CREATEDAT,
                    new Condition().withComparisonOperator(ComparisonOperator.BETWEEN).withAttributeValueList(
                            new AttributeValue().withN(Long.toString(timestamp)),
                            new AttributeValue().withN(Long.toString(startDateMilli))));

            QueryRequest queryRequest = new QueryRequest().withTableName(TABLE_NAME).withIndexName(INDEX_SCREENNAME)
                    .withKeyConditions(keyConditions).withSelect(Select.ALL_ATTRIBUTES).withScanIndexForward(true);

            QueryResult result = dynamoDB.query(queryRequest);
            return result;
        } catch (Exception e) {
            handleException(e);
        }

        return null;
    }

    public ScanResult getSince(long timestamp, int limit) throws Exception {
        try {
            Condition scanFilterCondition = new Condition().withComparisonOperator(ComparisonOperator.GT.toString())
                    .withAttributeValueList(new AttributeValue().withN(Long.toString(timestamp)));
            Map<String, Condition> conditions = new HashMap<String, Condition>();
            conditions.put(COL_CREATEDAT, scanFilterCondition);

            ScanRequest scanRequest = new ScanRequest().withTableName(TABLE_NAME).withScanFilter(conditions)
                    .withLimit(limit).withAttributesToGet(
                            Arrays.asList(COL_ID, COL_CREATEDAT, COL_LAT, COL_LONG, COL_SCREENNAME, COL_TEXT));

            return dynamoDB.scan(scanRequest);
        } catch (Exception e) {
            handleException(e);
        }

        return null;
    }

    private void handleException(Exception e) throws Exception {
        if (e instanceof AmazonServiceException) {
            AmazonServiceException ase = (AmazonServiceException) e;
            LOG.error("Caught an AmazonServiceException, which means your request made it "
                    + "to AWS, but was rejected with an error response for some reason.", e);
            LOG.error("Error Message:    " + ase.getMessage());
            LOG.error("HTTP Status Code: " + ase.getStatusCode());
            LOG.error("AWS Error Code:   " + ase.getErrorCode());
            LOG.error("Error Type:       " + ase.getErrorType());
            LOG.error("Request ID:       " + ase.getRequestId());
        } else if (e instanceof AmazonClientException) {
            AmazonClientException ace = (AmazonClientException) e;
            LOG.error("Caught an AmazonClientException, which means the client encountered "
                    + "a serious internal problem while trying to communicate with AWS, "
                    + "such as not being able to access the network.", e);
            LOG.error("Error Message: " + ace.getMessage());
        }

        throw e;
    }

    private static Map<String, AttributeValue> newItem(Status status) {
        Map<String, AttributeValue> item = new HashMap<String, AttributeValue>();
        item.put(COL_ID, new AttributeValue().withN(Long.toString(status.getId())));
        item.put(COL_CREATEDAT, new AttributeValue().withN(Long.toString(status.getCreatedAt().getTime())));
        if (status.getGeoLocation() != null) {
            item.put(COL_LAT, new AttributeValue().withN(Double.toString(status.getGeoLocation().getLatitude())));
            item.put(COL_LONG, new AttributeValue().withN(Double.toString(status.getGeoLocation().getLongitude())));
        }
        item.put(COL_SCREENNAME, new AttributeValue().withS(status.getUser().getScreenName()));
        item.put(COL_TEXT, new AttributeValue().withS(status.getText()));
        return item;
    }

    private static void waitForTableToBecomeAvailable(String tableName) {
        LOG.info("Waiting for " + tableName + " to become ACTIVE...");

        long startTime = System.currentTimeMillis();
        long endTime = startTime + (10 * 60 * 1000);
        while (System.currentTimeMillis() < endTime) {
            try {
                Thread.sleep(1000 * 20);
            } catch (Exception e) {
            }
            try {
                DescribeTableRequest request = new DescribeTableRequest().withTableName(tableName);
                TableDescription tableDescription = dynamoDB.describeTable(request).getTable();
                String tableStatus = tableDescription.getTableStatus();
                System.out.println("  - current state: " + tableStatus);
                if (tableStatus.equals(TableStatus.ACTIVE.toString()))
                    return;
            } catch (AmazonServiceException ase) {
                if (ase.getErrorCode().equalsIgnoreCase("ResourceNotFoundException") == false)
                    throw ase;
            }
        }

        throw new RuntimeException("Table " + tableName + " never went active");
    }

}