com.duboisproject.rushhour.database.SdbInterface.java Source code

Java tutorial

Introduction

Here is the source code for com.duboisproject.rushhour.database.SdbInterface.java

Source

/*
 * Dubois Traffic Puzzle
 * Jakob Cornell, 2017
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

package com.duboisproject.rushhour.database;

import android.os.SystemClock;

import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.ArrayList;
import java.util.Set;
import java.util.HashSet;
import java.util.Map;
import java.util.HashMap;
import java.util.UUID;
import java.io.StringReader;
import com.amazonaws.AmazonClientException;
import com.amazonaws.services.simpledb.AmazonSimpleDBClient;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.services.simpledb.model.GetAttributesRequest;
import com.amazonaws.services.simpledb.model.GetAttributesResult;
import com.amazonaws.services.simpledb.model.PutAttributesRequest;
import com.amazonaws.services.simpledb.model.Attribute;
import com.amazonaws.services.simpledb.model.ReplaceableAttribute;
import com.amazonaws.services.simpledb.model.SelectRequest;
import com.amazonaws.services.simpledb.model.SelectResult;
import com.amazonaws.services.simpledb.model.Item;
import com.amazonaws.services.simpledb.model.DomainMetadataRequest;

import org.joda.time.DateTime;
import org.joda.time.Duration;

import com.duboisproject.rushhour.Board;
import com.duboisproject.rushhour.BoardLoader;
import com.duboisproject.rushhour.GameStatistics;
import com.duboisproject.rushhour.id.Mathlete;
import com.duboisproject.rushhour.id.Coach;

public final class SdbInterface {
    protected long lastCoachScan = -13;
    private long max_time_between_coach_scans;

    protected static final String PRIMARY_KEY = "itemName()";

    // domain names
    protected static final String TIMELIMIT_DOMAIN = "dubois_rushhour_timelimit";
    protected static final String MATHLETE_DOMAIN = "dubois_mathlete_identities";
    protected static final String COACH_DOMAIN = "dubois_coach_identities";
    protected static final String LEVELS_DOMAIN = "dubois_rushhour_levels";
    protected static final String PLAYS_DOMAIN = "dubois_rushhour_games_played";

    // attributes: timelimit table
    protected static final String TIME_LIMIT = "time_limit";

    // attributes: mathletes table
    protected static final String MATHLETE_NAME = "name";
    protected static final String MATHLETE_LAST_NAME = "last_name";

    // attributes: coaches table
    protected static final String COACH_NAME = "Name";

    // attributes: levels table
    protected static final String LEVEL_MAP = "map";
    protected static final String LEVEL_DIFFICULTY = "difficulty";

    // attributes: plays table
    protected static final String PLAYS_MATHLETE = "mathlete";
    protected static final String PLAYS_LEVEL = "level_id";
    protected static final String PLAYS_TOTAL_MOVES = "total_moves";
    protected static final String PLAYS_RESET_MOVES = "reset_moves";
    protected static final String PLAYS_START = "start_time";
    protected static final String PLAYS_TOTAL_TIME = "total_time";
    protected static final String PLAYS_RESET_TIME = "reset_time";

    protected static Integer cachedLevelCount;
    protected static Integer cachedMaxDifficulty;

    protected static final String REQUEST_FAILED_MESSAGE = "Unable to complete request";

    protected static enum GetRequestDetails {
        TIMELIMIT(TIMELIMIT_DOMAIN, new String[] { TIME_LIMIT }), MATHLETE_ID(MATHLETE_DOMAIN,
                new String[] { MATHLETE_NAME, MATHLETE_LAST_NAME }), COACH_ID(COACH_DOMAIN,
                        new String[] { COACH_NAME }), MAP_FETCH(LEVELS_DOMAIN,
                                new String[] { LEVEL_MAP }), DIFFICULTY_FETCH(LEVELS_DOMAIN,
                                        new String[] { LEVEL_DIFFICULTY });

        public final String domainName;
        public final Collection<String> attributeNames;

        private GetRequestDetails(String domainName, String[] attributeNames) {
            this.domainName = domainName;
            this.attributeNames = Arrays.asList(attributeNames);
        }

        public GetAttributesRequest toAttributesRequest() {
            GetAttributesRequest request = new GetAttributesRequest();
            request.setDomainName(domainName);
            request.setAttributeNames(attributeNames);
            request.setConsistentRead(true);
            return request;
        }
    }

    public static final class RequestException extends Exception {
        public RequestException() {
        }

        public RequestException(String message) {
            super(message);
        }
    }

    public static final class TimeoutExceptionA extends Exception {
        public TimeoutExceptionA() {
        }

        public TimeoutExceptionA(String message) {
            super(message);
        }
    }

    protected final AmazonSimpleDBClient client;

    public SdbInterface(AWSCredentials credentials) {
        client = new AmazonSimpleDBClient(credentials);
    }

    public int timeLimit(String id) throws RequestException {
        GetAttributesRequest request = GetRequestDetails.TIMELIMIT.toAttributesRequest();
        request.setItemName(id);
        GetAttributesResult result;
        try {
            result = client.getAttributes(request);
        } catch (AmazonClientException e) {
            throw new RequestException(REQUEST_FAILED_MESSAGE);
        }
        List<Attribute> attributesList = result.getAttributes();
        if (attributesList.size() == 0) {
            throw new IllegalArgumentException("Internal error #1 timeLimit SdbInterface.java");
        }
        Map<String, String> attributes = mapify(attributesList);
        return Integer.parseInt(attributes.get(TIME_LIMIT));
    }

    public Mathlete fetchMathlete(String id) throws RequestException {
        GetAttributesRequest request = GetRequestDetails.MATHLETE_ID.toAttributesRequest();
        request.setItemName(id);
        GetAttributesResult result;
        try {
            result = client.getAttributes(request);
        } catch (AmazonClientException e) {
            throw new RequestException(REQUEST_FAILED_MESSAGE);
        }
        List<Attribute> attributesList = result.getAttributes();
        if (attributesList.size() == 0) {
            throw new IllegalArgumentException("No such mathlete in database");
        }
        Map<String, String> attributes = mapify(attributesList);
        return new Mathlete(id, attributes.get(MATHLETE_NAME), attributes.get(MATHLETE_LAST_NAME));
    }

    public Coach fetchCoach(String id) throws RequestException {
        GetAttributesRequest request = GetRequestDetails.COACH_ID.toAttributesRequest();
        request.setItemName(id);
        GetAttributesResult result;
        try {
            result = client.getAttributes(request);
        } catch (AmazonClientException e) {
            throw new RequestException(REQUEST_FAILED_MESSAGE);
        }
        List<Attribute> attributesList = result.getAttributes();
        if (attributesList.size() == 0) {
            throw new IllegalArgumentException("No such coach in database");
        }
        Map<String, String> attributes = mapify(attributesList);
        lastCoachScan = SystemClock.elapsedRealtime();
        max_time_between_coach_scans = timeLimit("1");

        return new Coach(id, attributes.get(COACH_NAME));
    }

    /**
     * Determine if another coach chip scan is necessary before continuing to play
     */
    public boolean isCoachCheckRequired() {
        return ((SystemClock.elapsedRealtime() - lastCoachScan) > max_time_between_coach_scans * 1000);
    }

    /**
     * Get the map for the specified level ID, and construct a board.
     */
    public Board fetchBoard(int id) throws RequestException {
        GetAttributesRequest request = GetRequestDetails.MAP_FETCH.toAttributesRequest();
        request.setItemName(Integer.toString(id));
        GetAttributesResult result;
        try {
            result = client.getAttributes(request);
        } catch (AmazonClientException e) {
            throw new RequestException(REQUEST_FAILED_MESSAGE);
        }
        List<Attribute> attributesList = result.getAttributes();
        if (attributesList.isEmpty()) {
            throw new IllegalArgumentException("No such level in database");
        }
        Map<String, String> attributes = mapify(attributesList);
        String map = attributes.get(LEVEL_MAP);
        Board board = BoardLoader.loadBoard(new StringReader(map));
        board.id = id;
        return board;
    }

    /**
     * Get the difficulty of the specified level ID.
     */
    public int fetchDifficulty(int id) throws RequestException {
        GetAttributesRequest request = GetRequestDetails.DIFFICULTY_FETCH.toAttributesRequest();
        request.setItemName(Integer.toString(id));
        GetAttributesResult result;
        try {
            result = client.getAttributes(request);
        } catch (AmazonClientException e) {
            throw new RequestException(REQUEST_FAILED_MESSAGE);
        }

        List<Attribute> attributesList = result.getAttributes();
        if (attributesList.isEmpty()) {
            throw new IllegalArgumentException("No such level in database");
        }
        Map<String, String> attributes = mapify(attributesList);
        return Integer.parseInt(attributes.get(LEVEL_DIFFICULTY));
    }

    /**
     * Get the total number of levels: the number of records in the levels table.
     */
    public int fetchLevelCount() throws RequestException {
        // We don't expect this to change during app runs, so we'll cache it.
        if (cachedLevelCount == null) {
            DomainMetadataRequest request = new DomainMetadataRequest(LEVELS_DOMAIN);
            try {
                cachedLevelCount = client.domainMetadata(request).getItemCount();
            } catch (AmazonClientException e) {
                throw new RequestException(REQUEST_FAILED_MESSAGE);
            }
        }
        return cachedLevelCount;
    }

    /**
     * Get the highest difficulty value in the levels table.
     * This copies all level difficulties into memory to sort.
     * It would be simpler and more performant to pad difficulties in the database and sort (lexicographically) there.
     */
    public int fetchMaxDifficulty() throws RequestException {
        // We'll cache this too.
        if (cachedMaxDifficulty == null) {
            String format = "select `%s` from `%s` limit 2500";
            String query = String.format(format, sdbEscape(LEVEL_DIFFICULTY, '`'), sdbEscape(LEVELS_DOMAIN, '`'));

            SelectRequest request = new SelectRequest(query, true);
            SelectResult result;
            try {
                result = client.select(request);
            } catch (AmazonClientException e) {
                throw new RequestException(REQUEST_FAILED_MESSAGE);
            }

            int max = Integer.MIN_VALUE;
            for (Item i : result.getItems()) {
                List<Attribute> attributes = i.getAttributes();
                if (!attributes.isEmpty()) {
                    int current = Integer.parseInt(attributes.get(0).getValue());
                    if (current > max) {
                        max = current;
                    }
                }
            }
            cachedMaxDifficulty = max;
        }
        return cachedMaxDifficulty;
    }

    /**
     * Add a play to the database.
     */
    public void putStats(Mathlete player, GameStatistics stats) throws RequestException {
        Map<String, String> attributes = new HashMap<String, String>();
        attributes.put(PLAYS_MATHLETE, player.id);
        attributes.put(PLAYS_LEVEL, Integer.toString(stats.levelId));
        attributes.put(PLAYS_TOTAL_MOVES, Integer.toString(stats.totalMoves));
        attributes.put(PLAYS_RESET_MOVES, Integer.toString(stats.resetMoves));
        attributes.put(PLAYS_START, stats.startTime.toString());
        attributes.put(PLAYS_TOTAL_TIME, stats.totalCompletionTime.toString());
        attributes.put(PLAYS_RESET_TIME, stats.resetCompletionTime.toString());

        PutAttributesRequest request = new PutAttributesRequest();
        request.setDomainName(PLAYS_DOMAIN);
        request.setItemName(UUID.randomUUID().toString());
        request.setAttributes(listify(attributes));

        try {
            client.putAttributes(request);
        } catch (AmazonClientException e) {
            throw new RequestException(REQUEST_FAILED_MESSAGE);
        }
    }

    /**
     * Fetch the stats for a mathlete's most recent play.
     *
     * Uses at most one query, and sorts on the database side.
     *
     * @return stats for the last play, or <code>null</code> if no plays exist
     */
    public GameStatistics fetchLastPlay(Mathlete mathlete) throws RequestException {
        String format = "select * from `%s` where `%s` = \"%s\" and `%s` is not null "
                + "order by `%s` desc limit 1";
        String query = String.format(format, sdbEscape(PLAYS_DOMAIN, '`'), sdbEscape(PLAYS_MATHLETE, '`'),
                sdbEscape(mathlete.id, '"'), sdbEscape(PLAYS_START, '`'), sdbEscape(PLAYS_START, '`'));

        SelectRequest request = new SelectRequest(query, true);
        SelectResult result;
        try {
            result = client.select(request);
        } catch (AmazonClientException e) {
            throw new RequestException(REQUEST_FAILED_MESSAGE);
        }

        List<Item> items = result.getItems();
        if (items.isEmpty()) {
            return null;
        } else {
            return parseStats(items.get(0));
        }
    }

    /**
     * Get the stats for all levels a mathlete has played.
     */
    public Set<GameStatistics> fetchAllPlays(Mathlete mathlete) throws RequestException {
        String format = "select * from `%s` where `%s` = \"%s\" limit 2500";
        String query = String.format(format, sdbEscape(PLAYS_DOMAIN, '`'), sdbEscape(PLAYS_MATHLETE, '`'),
                sdbEscape(mathlete.id, '"'));

        Set<GameStatistics> plays = new HashSet<GameStatistics>();

        SelectRequest request = new SelectRequest(query, true);
        String nextToken = null;
        do {
            request.setNextToken(nextToken);
            SelectResult result = client.select(request);
            for (Item item : result.getItems()) {
                plays.add(parseStats(item));
            }
            nextToken = result.getNextToken();
        } while (nextToken != null);

        return plays;
    }

    /**
     * Get the stats for all plays by a mathlete at a given difficulty.
     *
     * May be useful for determining which level in a certain difficulty should be played next.
     */
    public Map<Integer, GameStatistics[]> fetchStatsAtDifficulty(Mathlete player, int difficulty)
            throws RequestException {
        Map<Integer, GameStatistics[]> levelStats = new HashMap<Integer, GameStatistics[]>();

        // Select all levels of the specified difficulty
        String format = "select `%s` from `%s` where `%s` = \"%s\"";
        String query = String.format(format, sdbEscape(PRIMARY_KEY, '`'), sdbEscape(LEVELS_DOMAIN, '`'),
                sdbEscape(LEVEL_DIFFICULTY, '`'), sdbEscape(Integer.toString(difficulty), '"'));

        SelectRequest request = new SelectRequest(query, true);
        SelectResult result;
        try {
            result = client.select(request);
        } catch (AmazonClientException e) {
            throw new RequestException(REQUEST_FAILED_MESSAGE);
        }

        // Get all stats for each such level
        format = "select * from `%s` where `%s` = \"%s\" and `%s` = \"%s\"";
        for (Item item : result.getItems()) {
            int id = Integer.parseInt(item.getName());
            query = String.format(format, sdbEscape(PLAYS_DOMAIN, '`'), sdbEscape(PLAYS_LEVEL, '`'),
                    sdbEscape(Integer.toString(id), '"'), sdbEscape(PLAYS_MATHLETE, '`'),
                    sdbEscape(player.id, '"'));

            SelectRequest playsRequest = new SelectRequest(query, true);
            SelectResult playsResult;
            try {
                playsResult = client.select(playsRequest);
            } catch (AmazonClientException e) {
                throw new RequestException(REQUEST_FAILED_MESSAGE);
            }

            List<Item> items = playsResult.getItems();
            GameStatistics[] stats = new GameStatistics[items.size()];
            for (int i = 0; i < items.size(); i += 1) {
                stats[i] = parseStats(items.get(i));
            }
            levelStats.put(id, stats);
        }
        return levelStats;
    }

    /**
     * Get the IDs for all levels at a certain difficulty.
     */
    public int[] fetchLevelsAtDifficulty(int difficulty) throws RequestException {
        String format = "select `%s` from `%s` where `%s` = \"%s\" limit 2500";
        String query = String.format(format, sdbEscape(PRIMARY_KEY, '`'), sdbEscape(LEVELS_DOMAIN, '`'),
                sdbEscape(LEVEL_DIFFICULTY, '`'), sdbEscape(Integer.toString(difficulty), '"'));

        SelectRequest request = new SelectRequest(query, true);
        SelectResult result;
        try {
            result = client.select(request);
        } catch (AmazonClientException e) {
            throw new RequestException(REQUEST_FAILED_MESSAGE);
        }

        List<Item> items = result.getItems();
        int[] levelIds = new int[items.size()];
        for (int i = 0; i < items.size(); i += 1) {
            levelIds[i] = Integer.parseInt(items.get(i).getName());
        }
        return levelIds;
    }

    /**
     * Escapes a string (domain name, attrubute name/value) for use in SDB select statements.
     *
     * This process involves "expanding" certain characters depending on context.
     * See <a href="http://docs.aws.amazon.com/AmazonSimpleDB/latest/DeveloperGuide/QuotingRulesSelect.html">quoting rules</a>.
     *
     * @param value  the string to be escaped
     * @param special  the special character to be expanded
     * @return  the escaped string
     */
    protected static String sdbEscape(String value, char special) {
        String from = Character.toString(special);
        String to = from + from;
        return value.replace(from, to);
    }

    protected static GameStatistics parseStats(Item item) {
        Map<String, String> attributes = mapify(item.getAttributes());
        GameStatistics stats = new GameStatistics();
        stats.levelId = Integer.parseInt(attributes.get(PLAYS_LEVEL));
        stats.totalMoves = Integer.parseInt(attributes.get(PLAYS_TOTAL_MOVES));
        stats.resetMoves = Integer.parseInt(attributes.get(PLAYS_RESET_MOVES));
        stats.startTime = DateTime.parse(attributes.get(PLAYS_START));
        stats.totalCompletionTime = Duration.parse(attributes.get(PLAYS_TOTAL_TIME));
        stats.resetCompletionTime = Duration.parse(attributes.get(PLAYS_RESET_TIME));
        return stats;
    }

    protected static Map<String, String> mapify(List<Attribute> attributes) {
        Map<String, String> attributesMap = new HashMap<String, String>();
        for (Attribute a : attributes) {
            attributesMap.put(a.getName(), a.getValue());
        }
        return attributesMap;
    }

    protected static List<ReplaceableAttribute> listify(Map<String, String> attributes) {
        List<ReplaceableAttribute> attributesList = new ArrayList<ReplaceableAttribute>();
        for (Map.Entry<String, String> entry : attributes.entrySet()) {
            attributesList.add(new ReplaceableAttribute(entry.getKey(), entry.getValue(), true));
        }
        return attributesList;
    }
}