com.tfgridiron.crowdsource.cmdline.GameCharter.java Source code

Java tutorial

Introduction

Here is the source code for com.tfgridiron.crowdsource.cmdline.GameCharter.java

Source

/*
 * Copyright (c) Justin Moore
 * 
 * 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.
 */

package com.tfgridiron.crowdsource.cmdline;

import com.bcsreport.cfbstats.tables.PlayRow;
import com.bcsreport.cfbstats.tables.TeamGameStatsRow;
import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.extensions.java6.auth.oauth2.AuthorizationCodeInstalledApp;
import com.google.api.client.extensions.jetty.auth.oauth2.LocalServerReceiver;
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow;
import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets;
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.client.util.DateTime;
import com.google.api.client.util.store.DataStoreFactory;
import com.google.api.client.util.store.FileDataStoreFactory;
import com.google.api.services.drive.Drive;
import com.google.api.services.drive.Drive.Children;
import com.google.api.services.drive.DriveScopes;
import com.google.api.services.drive.model.ChildReference;
import com.google.api.services.drive.model.File;
import com.google.api.services.drive.model.FileList;
import com.google.api.services.drive.model.ParentReference;
import com.google.api.services.drive.model.Permission;
import com.google.gdata.client.spreadsheet.SpreadsheetService;
import com.google.gdata.data.spreadsheet.SpreadsheetEntry;
import com.google.gdata.util.ServiceException;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * The stand-alone game-charting administration application for Football Study Hall's Game Charting
 * project.
 * <ul>
 * <li>Does stuff</li>
 * </ul>
 * 
 * @author justin@tfgridiron.com (Justin Moore)
 */
public class GameCharter {
    // Top-level tasks:
    // TODO(P1): Add 'charter (un)assign' support
    // TODO(P1): Break this file up more
    // TODO(P1): Implement 'forcechecksums'
    // TODO(P2): Progress bars everywhere
    private static final String SET_PARENT_FOLDER_COMMAND = "setparentfolder";
    private static final String SET_DATA_DIRECTORY_COMMAND = "setdatadirectory";
    private static final String ADMIN_COMMAND = "admin";
    private static final String CHARTER_COMMAND = "charter";
    private static final String GAME_COMMAND = "game";
    private static final String FORCE_CHECKSUMS_COMMAND = "forcechecksums";
    private static final String ARCHIVE_COMMAND = "archive";
    private static final Set<String> VALID_COMMANDS = makeValidCommands();
    private static final String GAME_ADDNEW_COMMAND = "new";

    /** Directory to store user credentials. */
    private static final java.io.File DATA_STORE_DIR = new java.io.File(Constants.BASE_DIRECTORY, "store");

    private static final int MAX_CREATION_RETRIES = 2;

    private static final Pattern TEAM_ID_PARSER = Pattern.compile("^0*(\\d+)$");

    /**
     * Property object which stores all of the configuration data.
     */
    private static Properties properties;

    /**
     * ID of the file we use as the template for all other play-by-play documents
     */
    private static String playByPlayTemplateId;

    /**
     * Global instance of the {@link DataStoreFactory}.
     */
    private static FileDataStoreFactory dataStoreFactory;

    /** Global instance of the HTTP transport. */
    private static HttpTransport httpTransport;

    /** Global instance of the JSON factory. */
    private static final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance();

    /** Credentials for the Google APIs. */
    private static Credential credential;

    /** Global Drive API client. */
    private static Drive drive;

    /** Global Spreadsheet API client. */
    private static SpreadsheetService spreadsheetService;

    /** Utils for convenience functions to the API */
    private static ApiUtils apiUtils;

    /** Global Spreadsheet index metadata */
    private static SpreadsheetIndexer spreadsheetIndexer;
    private static AssignmentIndexer assignmentIndexer;
    private static TeamIndexer teamIndexer;
    private static ArchiveIndexer archiveIndexer;
    private static ArchiveCreator archiveCreator;
    private static GameSpreadsheetCreator gameSpreadsheetCreator;

    /** Maps pairs of (season, week) to the IDs of the folders in which they should be placed */
    private static Map<Integer, Map<String, String>> seasonToFolderId;

    /** Data for the current season from the local data directory */
    private static DataArchiveParser localSeasonData;

    private enum GameInfoColumns {
        GAME_DATE(0), HOME_ID(1), HOME_NAME(2), HOME_POINTS(3), AWAY_ID(4), AWAY_NAME(5), AWAY_POINTS(6);

        private int index;

        private GameInfoColumns(int index) {
            this.index = index;
        }

        public int getValue() {
            return index;
        }
    }

    public static void main(String[] args) {
        System.out.println(Arrays.toString(args));
        if (args.length == 0) {
            printUsage(0);
        }
        if (!VALID_COMMANDS.contains(args[0])) {
            System.err.println("Invalid command: " + args[0]);
            printUsage(1);
        }
        try {
            if (SET_PARENT_FOLDER_COMMAND.equals(args[0])) {
                setParentFolder(args);
                System.exit(0);
            } else if (SET_DATA_DIRECTORY_COMMAND.equals(args[0])) {
                setDataDirectory(args);
                System.exit(0);
            }
            loadConfiguration(true);
            authorize();
            connectToServices();
            loadIndexes();
            if (ADMIN_COMMAND.equals(args[0])) {
                adminOp(args);
            } else if (GAME_COMMAND.equals(args[0])) {
                loadDataDirectory();
                gameOp(args);
            } else if (CHARTER_COMMAND.equals(args[0])) {
                charterOp(args);
            } else if (FORCE_CHECKSUMS_COMMAND.equals(args[0])) {
                forceChecksums(args);
            } else if (ARCHIVE_COMMAND.equals(args[0])) {
                archiveOp(args);
            } else {
                System.err.println("Unhandled command: " + args[0]);
                printUsage(1);
            }
            System.exit(0);
        } catch (IOException e) {
            System.err.println(e.getMessage());
        } catch (Throwable t) {
            t.printStackTrace();
        }
        System.exit(1);
    }

    private static void connectToServices() throws Exception {
        if (credential == null) {
            throw new Exception("No valid credentials present; cannot connect to services.");
        }
        // set up the global Drive instance
        drive = new Drive.Builder(httpTransport, JSON_FACTORY, credential)
                .setApplicationName(Constants.APPLICATION_NAME).build();
        cachePlayByPlayTemplateId();
        getPerYearFolders();
        spreadsheetService = new SpreadsheetService(Constants.APPLICATION_NAME);
        spreadsheetService.setOAuth2Credentials(credential);
        apiUtils = new ApiUtils(drive, spreadsheetService,
                properties.getProperty(Constants.PARENT_FOLDER_PROPERTY));
    }

    private static void loadIndexes() throws Exception {
        if (apiUtils == null) {
            throw new Exception("Cannot parse index doc; not connected to Google services yet");
        }
        teamIndexer = new TeamIndexer(apiUtils);
        teamIndexer.loadIndex();
        assignmentIndexer = new AssignmentIndexer(apiUtils, teamIndexer);
        assignmentIndexer.loadIndex();
        spreadsheetIndexer = new SpreadsheetIndexer(apiUtils, assignmentIndexer);
        spreadsheetIndexer.loadIndex();
    }

    protected static void printUsage(int exitCode) {
        // TODO(P0): Expand this to be a real usage
        System.out.println("List of valid commands:");
        for (String command : VALID_COMMANDS) {
            System.out.println("  " + command);
        }
        System.exit(exitCode);
    }

    protected static void setParentFolder(String[] args) throws Exception {
        if (args.length != 2) {
            System.err.println("No parent folder URL specified");
            printUsage(1);
        }
        try {
            loadConfiguration(false);
        } catch (Exception e) {
            // Continue on our merry way.
        }
        if (properties == null) {
            properties = new Properties();
        }
        URL parentFolder = null;
        try {
            parentFolder = new URL(args[1]);
        } catch (MalformedURLException e) {
            e.printStackTrace(System.err);
            System.exit(1);
        }
        if (!Constants.GOOGLE_DRIVE_HOST.equals(parentFolder.getHost())) {
            System.err.println("Invalid Google Drive hostname: " + parentFolder.getHost());
            System.exit(1);
        }
        Matcher m = Constants.FOLDER_ID_EXTRACTOR.matcher(parentFolder.getRef());
        if (!m.matches()) {
            System.err.println("Invalid folder reference: " + parentFolder.getRef());
            System.err.println("Are you sure this is actually a Google Drive Folder?");
            System.exit(1);
        }
        properties.setProperty(Constants.PARENT_FOLDER_PROPERTY, m.group(1));
        storeConfiguration();
        System.out.println(Constants.PARENT_FOLDER_PROPERTY + " is set to " + m.group(1));
    }

    protected static void setDataDirectory(String[] args) throws Exception {
        if (args.length != 2) {
            System.err.println("No data directory specified");
            printUsage(1);
        }
        try {
            loadConfiguration(false);
        } catch (Exception e) {
            // Continue on our merry way.
        }
        if (properties == null) {
            properties = new Properties();
        }
        java.io.File dataDirectory = new java.io.File(args[1]);
        if (!dataDirectory.exists()) {
            System.err.println("Specified data directory does not exist: " + args[1]);
            printUsage(1);
        }
        if (!dataDirectory.isDirectory()) {
            System.err.println("Specified data directory path is not a directory: " + args[1]);
            printUsage(1);
        }
        properties.setProperty(Constants.DATA_DIRECTORY_PROPERTY, args[1]);
        storeConfiguration();
        System.out.println(Constants.DATA_DIRECTORY_PROPERTY + "is set to " + args[1]);
    }

    protected static void createIndex(String[] args) throws Exception {
        throw new Exception("createIndex not yet implemented");
    }

    protected static void adminOp(String[] args) throws Exception {
        if (args.length != 3) {
            System.err.println("Invalid 'admin' operation");
            printUsage(1);
        }
        if (args[1].equals("add")) {
            assignmentIndexer.addAdmin(args[2]);
        } else if (args[1].equals("del")) {
            assignmentIndexer.delAdmin(args[2]);
        } else {
            System.err.println("Invalid 'admin' command: " + args[2]);
            printUsage(1);
        }
    }

    protected static void charterOp(String[] args) throws Exception {
        if (args.length < 2) {
            System.err.println("Invalid 'charter' operation");
            printUsage(1);
        }
        if (args[1].equals("set")) {
            if (args.length == 4) {
                assignmentIndexer.setCharter(args[2], Integer.parseInt(args[3]), null);
            } else if (args.length == 5) {
                assignmentIndexer.setCharter(args[2], Integer.parseInt(args[3]), Integer.parseInt(args[4]));
            } else {
                System.err.println("Invalid 'charter set' command");
                printUsage(1);
            }
        } else if (args[1].equals("del")) {
            if (args.length < 3) {
                System.err.println("Invalid 'charter del' operation");
                printUsage(1);
            }
            Set<Integer> teamIds = new HashSet<Integer>();
            for (int i = 3; i < args.length; ++i) {
                teamIds.add(Integer.parseInt(args[i]));
            }
            assignmentIndexer.delCharter(args[2], teamIds);
        } else if (args[1].equals("assign")) {
            throw new Exception("'charter assign' operation not yet implemented");
        } else if (args[1].equals("unassign")) {
            throw new Exception("'charter unassign' operation not yet implemented");
        } else {
            System.err.println("Invalid 'charter' operation");
            printUsage(1);
        }
    }

    protected static void gameOp(String[] args) throws Exception {
        if (args.length < 2) {
            System.err.println("Invalid 'game' operation.");
            printUsage(1);
        }
        // By default, add all the games in the file.
        String gameId = GAME_ADDNEW_COMMAND;
        boolean dry_run = false;
        if (args[1].startsWith("test")) {
            args[1] = args[1].substring(4);
            dry_run = true;
        }
        if (args[1].equals("add")) {
            if (args.length != 3) {
                System.err.println("Invalid 'game add' command.");
                printUsage(1);
            }
            gameId = args[2];
        }
        cloneGame(gameId, dry_run);
    }

    protected static void forceChecksums(String[] args) throws Exception {
        throw new Exception("forceChecksums not yet implemented");
    }

    protected static void archiveOp(String[] args) throws Exception {
        if (drive == null || spreadsheetService == null) {
            throw new Exception("Missing connection to either Drive or Spreadsheets");
        }
        if (args.length != 3) {
            System.err.println("Invalid archive operation.");
            printUsage(1);
        }
        archiveIndexer = new ArchiveIndexer(apiUtils);
        archiveIndexer.loadIndex();
        if (args[1].equals("make")) {
            archiveCreator = new ArchiveCreator(apiUtils);
            updateArchive(args[2]);
        } else {
            System.err.println("Invalid archive operation.");
            printUsage(1);
        }
    }

    protected static void loadDataDirectory() throws IOException {
        if (properties == null) {
            System.err.println("No properties set; please run the '" + SET_DATA_DIRECTORY_COMMAND + "' command.");
            printUsage(1);
        }
        String dataDirectory = properties.getProperty(Constants.DATA_DIRECTORY_PROPERTY);
        if (dataDirectory == null || dataDirectory.isEmpty()) {
            System.err.println("Data directory property not set; please run the '" + SET_DATA_DIRECTORY_COMMAND
                    + "' command.");
            printUsage(1);
        }
        localSeasonData = new DataArchiveParser(teamIndexer);
        localSeasonData.loadData(dataDirectory);
        gameSpreadsheetCreator = new GameSpreadsheetCreator(apiUtils, localSeasonData);
    }

    private static void loadConfiguration(boolean requireInitialization) throws Exception {
        if (properties == null) {
            properties = new Properties();
            InputStream inputStream = new FileInputStream(Constants.CONFIG_FILE);
            properties.load(inputStream);
        }
        if (requireInitialization) {
            checkProperty(Constants.PARENT_FOLDER_PROPERTY);
            checkProperty(Constants.DATA_DIRECTORY_PROPERTY);
        }
    }

    private static void storeConfiguration() throws Exception {
        if (properties == null) {
            throw new Exception("No configuration values to store");
        }
        if (properties.getProperty(Constants.PARENT_FOLDER_PROPERTY) == null) {
            throw new Exception("Configuration requires at least the locaton of the parent folder");
        }
        OutputStream outputStream = new FileOutputStream(Constants.CONFIG_FILE);
        properties.store(outputStream, Constants.CONFIG_FILE_HEADER);
        outputStream.close();
    }

    private static void checkProperty(String name) throws Exception {
        if (properties == null || name == null) {
            throw new Exception("Configuration file not loaded or missing configuration property name");
        }
        if (properties.getProperty(name) == null) {
            throw new Exception("Configuration property " + name + " is missing from " + Constants.CONFIG_FILE);
        }
    }

    private static void authorize() throws Exception {
        httpTransport = GoogleNetHttpTransport.newTrustedTransport();
        dataStoreFactory = new FileDataStoreFactory(DATA_STORE_DIR);
        // load client secrets
        InputStream inputStream = GameCharter.class.getResourceAsStream("/client_secrets.json");
        if (inputStream == null) {
            inputStream = GameCharter.class.getResourceAsStream("client_secrets.json");
            if (inputStream == null) {
                inputStream = GameCharter.class.getResourceAsStream("resources/client_secrets.json");
                if (inputStream == null) {
                    inputStream = GameCharter.class.getResourceAsStream("/resources/client_secrets.json");
                }
            }
        }
        if (inputStream == null) {
            throw new IOException("Could not get client_secrets.json from ... anywhere");
        }
        GoogleClientSecrets clientSecrets = GoogleClientSecrets.load(JSON_FACTORY,
                new InputStreamReader(inputStream));
        if (clientSecrets.getDetails().getClientId().startsWith("Enter")
                || clientSecrets.getDetails().getClientSecret().startsWith("Enter ")) {
            System.err.println("Internal erorr: Enter Client ID and Secret from "
                    + "https://code.google.com/apis/console/?api=drive into client_secrets.json");
            System.exit(1);
        }
        Set<String> scopes = new HashSet<String>(2);
        scopes.add(DriveScopes.DRIVE);
        scopes.add(Constants.OAUTH_SPREADSHEET_SCOPE);
        // set up authorization code flow
        GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow.Builder(httpTransport, JSON_FACTORY,
                clientSecrets, scopes).setDataStoreFactory(dataStoreFactory).build();
        // authorize
        credential = new AuthorizationCodeInstalledApp(flow, new LocalServerReceiver()).authorize("user");
    }

    private static void cloneGame(String targetGameId, boolean dry_run) throws Exception {
        if (targetGameId == null) {
            throw new IllegalArgumentException("No game ID provided to cloneGame().");
        }
        if (playByPlayTemplateId == null || playByPlayTemplateId.isEmpty()) {
            System.err.println("Cannot find 'Play-by-Play Template' file in the parent directory; cannot clone it");
            System.exit(1);
        }
        System.out.println("cloneGame(" + targetGameId + ")");
        Set<String> gameIds = localSeasonData.getSeason().getGameTable()
                .getMatchingGames(targetGameId.equals(GAME_ADDNEW_COMMAND) ? null : targetGameId);
        System.out.println("Matched " + gameIds.size() + " games");
        System.out.println("matching: " + gameIds);
        int MAX_TO_CLONE = 250;
        int num_cloned = 0;
        List<String> perGameInfo = new ArrayList<String>();
        for (String thisGameId : gameIds) {
            // TODO(P0): Remove this before distribution
            if (num_cloned >= MAX_TO_CLONE) {
                return;
            }
            String gameDate = thisGameId.substring(8);
            if (CalendarUtils.isFutureGame(gameDate)) {
                continue;
            }
            if (!gameIdToInfo(thisGameId, perGameInfo) || perGameInfo.size() != 7) {
                continue;
            }
            String season = CalendarUtils.gameDateToSeason(gameDate);
            String week = CalendarUtils.gameDateToWeek(gameDate);
            // TODO(P0): Make this happen automatically for per-year and per-season.
            Map<String, String> weekFolders = seasonToFolderId.get(Integer.parseInt(season));
            if (weekFolders == null) {
                System.err.println("There is no folder for season '" + season
                        + "' under the root play-by-play folder; please create it");
                continue;
            }
            String weekFolderId = weekFolders.get(week);
            if (weekFolderId == null) {
                System.err.println("There is no folder for week '" + week + "' in season '" + season + "'");
                continue;
            }
            List<PlayRow> gamePlays = localSeasonData.getSeason().getPlayTable().getPlays(thisGameId);
            // Does this game already exist?
            List<String> titles = new ArrayList<String>();
            Map<String, String> titleToCharter = new HashMap<String, String>();
            subGamesToClone(season, thisGameId, gameDate, perGameInfo, gamePlays, dry_run, getCharters(perGameInfo),
                    titles, titleToCharter);
            num_cloned += titles.size();
            // We now know the sets of (title, charter) tuples we're going to create. Clone the first game
            // from the blank template, populate the template with the play-by-play data, and then clone
            // the remaining ones off that.
            if (titles.isEmpty()) {
                continue;
            }
            Map<String, String> titleToId = new HashMap<String, String>();
            // System.out.println("Cloning and populating base data for game " + thisGameId + "; "
            // + titles.get(0));
            String baseFileId = cloneAndFillUsingTemplate(playByPlayTemplateId, season, week, titles.get(0),
                    thisGameId, perGameInfo, gamePlays);
            if (baseFileId == null) {
                continue;
            }
            titleToId.put(titles.get(0), baseFileId);
            for (int i = 1; i < titles.size(); ++i) {
                System.out.println("Cloning duplicate data for game " + thisGameId + "; from " + titles.get(0)
                        + " to " + titles.get(i));
                String fileId = cloneAndFillUsingTemplate(baseFileId, season, week, titles.get(i), thisGameId, null,
                        null);
                titleToId.put(titles.get(i), fileId);
            }
            for (String title : titles) {
                setFilePermissions(titleToId.get(title), titleToCharter.get(title));
                SpreadsheetEntry spreadsheet = apiUtils.getSpreadsheetByTitleAndFolder(title, weekFolderId);
                SpreadsheetMetadata metadata = spreadsheetIndexer.fetchPartialSpreadsheetMetadata(spreadsheet);
                metadata.setAssignedTo(titleToCharter.get(title));
                spreadsheetIndexer.updateAndSyncMetadata(season, spreadsheet, metadata, true);
            }
        }
    }

    private static String gameInfoToTitle(String gameId, String gameDate, List<String> perGameInfo, Integer index) {
        if (index == null) {
            index = 0;
        }
        String homeTeam = perGameInfo.get(GameInfoColumns.HOME_NAME.getValue());
        String awayTeam = perGameInfo.get(GameInfoColumns.AWAY_NAME.getValue());
        return gameDate + ": " + awayTeam + " at " + homeTeam + " (GID:" + gameId + "-" + index + ")";
    }

    private static Set<String> getCharters(List<String> perGameInfo) {
        String homeIdStr = perGameInfo.get(GameInfoColumns.HOME_ID.getValue());
        String awayIdStr = perGameInfo.get(GameInfoColumns.AWAY_ID.getValue());
        Set<String> charters = assignmentIndexer.getChartersForTeams(Integer.parseInt(homeIdStr),
                Integer.parseInt(awayIdStr));
        if (charters.isEmpty()) {
            charters = new HashSet<String>(1);
            charters.add("");
        }
        return charters;
    }

    private static void subGamesToClone(String season, String thisGameId, String gameDate, List<String> perGameInfo,
            List<PlayRow> thisGameData, boolean dry_run, Set<String> charters, List<String> titles,
            Map<String, String> titleToCharter) {
        int i = 0;
        Map<String, SpreadsheetMetadata> seasonMetadata = spreadsheetIndexer.getSpreadsheetMetadataBySeason(season);
        for (String charter : charters) {
            String title = gameInfoToTitle(thisGameId, gameDate, perGameInfo, i++);
            if (seasonMetadata == null || !seasonMetadata.containsKey(title)) {
                // We haven't indexed this game yet. It's likely we'll be creating a new spreadsheet.
                System.out.println("Creating new spreadsheet '" + title + "' (" + thisGameData.size()
                        + " plays); charted by " + charter);
                if (!dry_run) {
                    titles.add(title);
                    titleToCharter.put(title, charter);
                }
            } else {
                System.out.println("Skipping existing spreadsheet '" + title + "'");
            }
        }
    }

    private static void updateArchive(String season) throws Exception {
        System.out.println("updateArchive(" + season + ")");
        // Step 1: Refresh all the metadata
        spreadsheetIndexer.refreshMetadataFromSources(season, seasonToFolderId.get(season));
        Set<SpreadsheetMetadata> includedMetadata = new TreeSet<SpreadsheetMetadata>();
        // Step 2: Now that the Google Drive data is refreshed, grab a local copy of the metadata.
        Map<String, SpreadsheetMetadata> allFiles = spreadsheetIndexer.getSpreadsheetMetadataBySeason(season);
        if (allFiles != null) {
            Set<SpreadsheetMetadata> allMetadata = new HashSet<SpreadsheetMetadata>(allFiles.values());
            for (SpreadsheetMetadata spreadsheetMetadata : allMetadata) {
                if (spreadsheetMetadata.getUseThis()) {
                    includedMetadata.add(spreadsheetMetadata);
                }
            }
        }
        ArchiveMetadata archiveMetadata = archiveIndexer.getMetadataByTitle(season);
        // Scenarios:
        // 1) Completed spreadsheets exist, archived metadata does not: create archive
        // 2) Completed spreadsheets exist, archived metadata exists: update archive
        // 3) No completed spreadsheets exist, no archived data exists: bail
        // 4) No completed spreadsheets exist, archived data does: print warning, exit
        if (includedMetadata.isEmpty()) {
            System.out.println("No completed worksheets present");
            if (archiveMetadata != null) {
                // Scenario 4
                System.err.println(
                        "Archive for worksheet " + season + " exists, but no completed worksheets; bailing");
            } // else Scenario 3
            return;
        }
        if (archiveMetadata != null) {
            // TODO(P2): Move getMaxLastUpdated to spreadsheetIndexer
            DateTime maxLastUpdated = archiveCreator.getMaxLastUpdated(spreadsheetIndexer, includedMetadata);
            String checksum = archiveCreator.checksumFileCollection(spreadsheetIndexer, includedMetadata);
            if (archiveMetadata.getLastUpdated().getValue() >= maxLastUpdated.getValue()) {
                // This archive is newer than the set of files.
                System.out.println("Current archive is newer than all the spreadsheets in the archive.");
                return;
            }
            // The timestamp on the archive is older than the collective spreadsheet timestamp, but what
            // about the checksums?
            if (checksum.equals(archiveMetadata.getChecksum())) {
                System.out.println("Current archive is older than spreadsheets, but checksums are the same");
                archiveMetadata.setLastUpdated(maxLastUpdated);
                archiveIndexer.insertOrUpdateMetadata(archiveMetadata);
                return;
            }
        }
        System.out.println("Adding new archive " + season);
        // If we got here, we need to either add a new archive or update an existing one.
        ArchiveMetadata newMetadata = archiveCreator.createArchive(season, seasonToFolderId.get(season),
                spreadsheetIndexer, properties.getProperty(Constants.PARENT_FOLDER_PROPERTY));
        archiveIndexer.insertOrUpdateMetadata(newMetadata);
    }

    private static boolean gameIdToInfo(String gameId, List<String> gameInfo) throws Exception {
        String awayIdStr = gameId.substring(0, 4);
        String homeIdStr = gameId.substring(4, 8);
        String gameDate = gameId.substring(8);
        if (!TEAM_ID_PARSER.matcher(awayIdStr).matches() || !TEAM_ID_PARSER.matcher(homeIdStr).matches()) {
            System.err.println("Either away ID " + awayIdStr + " or home ID " + homeIdStr + " is invalid");
            return false;
        }
        if (!CalendarUtils.isValidGameDate(gameDate)) {
            return false;
        }
        int awayId = Integer.parseInt(awayIdStr);
        int homeId = Integer.parseInt(homeIdStr);
        String awayTeam = teamIndexer.getShortName(awayId);
        String homeTeam = teamIndexer.getShortName(homeId);
        if (awayTeam == null || homeTeam == null) {
            System.err.println("Game: " + gameId + "; one of these IDs is invalid: " + awayId + " or " + homeId);
            return false;
        }
        if (!localSeasonData.hasGameData(gameId)) {
            System.err.println("Missing game data for game " + gameId);
            return false;
        }
        TeamGameStatsRow homeStats = localSeasonData.getSeason().getTeamGameStatsTable().getTeamGameStats(gameId,
                homeId);
        TeamGameStatsRow awayStats = localSeasonData.getSeason().getTeamGameStatsTable().getTeamGameStats(gameId,
                awayId);
        if (homeStats == null) {
            System.err.println("Missing home stats for game " + gameId + " team " + homeId);
            return false;
        }
        if (awayStats == null) {
            System.err.println("Missing away stats for game " + gameId + " team " + awayId);
            return false;
        }
        gameInfo.clear();
        gameInfo.add(gameDate);
        gameInfo.add(Integer.toString(homeId));
        gameInfo.add(homeTeam);
        gameInfo.add(Integer.toString(homeStats.getPoints()));
        gameInfo.add(Integer.toString(awayId));
        gameInfo.add(awayTeam);
        gameInfo.add(Integer.toString(awayStats.getPoints()));
        return true;
    }

    private static void cachePlayByPlayTemplateId() throws Exception {
        String parentFolder = properties.getProperty(Constants.PARENT_FOLDER_PROPERTY);
        if (parentFolder == null || parentFolder.isEmpty()) {
            System.err.println("No parent folder set; must run 'setparent' command first");
            System.exit(1);
        }
        String q = "title = '" + Constants.PLAY_BY_PLAY_TEMPLATE_TITLE + "' and '" + parentFolder + "' in parents";
        FileList fileList = drive.files().list().setQ(q).execute();
        List<File> files = fileList.getItems();
        if (files.size() != 1) {
            System.err.println("Cannot find template file named '" + Constants.PLAY_BY_PLAY_TEMPLATE_TITLE
                    + "' in folder " + parentFolder);
            System.exit(1);
        }
        playByPlayTemplateId = files.get(0).getId();
    }

    private static String cloneAndFillUsingTemplate(String templateFileId, String season, String week,
            String fileTitle, String gameId, List<String> gameInfo, List<PlayRow> gamePlays) throws Exception {
        Map<String, String> weekFolders = seasonToFolderId.get(Integer.parseInt(season));
        if (weekFolders == null) {
            System.err.println("No folders for season " + season);
            return null;
        }
        String parentFolderId = weekFolders.get(week);
        if (parentFolderId == null) {
            System.err.println("No folder for season " + season + " week " + week);
            return null;
        }
        if (apiUtils.getSpreadsheetByTitleAndFolder(fileTitle, parentFolderId) != null) {
            System.out.println("\nAlready have an entry for spreadsheet " + fileTitle);
            return null;
        }
        ParentReference parent = new ParentReference();
        parent.setId(parentFolderId);
        File copiedFile = new File();
        copiedFile.setTitle(fileTitle);
        copiedFile.setParents(Collections.singletonList(parent));
        File newFile = drive.files().copy(templateFileId, copiedFile).execute();
        if (gameInfo != null && gamePlays != null) {
            SpreadsheetEntry newSpreadsheet = insertBaseData(newFile, gameId, fileTitle, gameInfo, gamePlays);
            if (newSpreadsheet == null) {
                System.err.println("Error inserting base data for " + fileTitle);
                return null;
            }
        }
        return newFile.getId();
    }

    private static void getPerYearFolders() throws IOException {
        if (seasonToFolderId != null) {
            return;
        }
        String rootParentId = properties.getProperty(Constants.PARENT_FOLDER_PROPERTY);
        if (rootParentId == null) {
            return;
        }
        seasonToFolderId = new HashMap<Integer, Map<String, String>>();
        String query = "mimeType = '" + Constants.FOLDER_MIME + "'";
        Children.List children = drive.children().list(rootParentId).setQ(query);
        for (ChildReference child : children.execute().getItems()) {
            File f = drive.files().get(child.getId()).execute();
            if (Constants.FOLDER_MIME.equals(f.getMimeType())) {
                try {
                    Integer year = Integer.parseInt(f.getTitle());
                    if (year == null || year < 2000 || year > 2050) {
                        continue;
                    }
                    seasonToFolderId.put(year, getPerWeekFolders(f.getId()));
                } catch (NumberFormatException nfe) {
                    continue;
                }
            }
        }
    }

    private static Map<String, String> getPerWeekFolders(String parentId) throws IOException {
        Map<String, String> weekFolders = new HashMap<String, String>();
        weekFolders.put(Constants.SEASON_FOLDER_TAG, parentId);
        String query = "mimeType = '" + Constants.FOLDER_MIME + "'";
        Children.List children = drive.children().list(parentId).setQ(query);
        for (ChildReference child : children.execute().getItems()) {
            File f = drive.files().get(child.getId()).execute();
            weekFolders.put(f.getTitle(), f.getId());
        }
        return weekFolders;
    }

    private static void setFilePermissions(String fileId, String charterUser) throws Exception {
        if (fileId == null || fileId.isEmpty()) {
            return;
        }
        for (String userEmail : assignmentIndexer.getAdmins()) {
            addWriter(fileId, userEmail);
        }
        if (charterUser != null && !charterUser.isEmpty()) {
            addWriter(fileId, charterUser);
        }
    }

    private static void addWriter(String fileId, String userEmail) throws Exception {
        Permission p = new Permission();
        p.setValue(userEmail).setRole("writer").setType("user");
        drive.permissions().insert(fileId, p).setSendNotificationEmails(false).execute();
    }

    private static void delWriter(String fileId, String userEmail) throws Exception {
        List<Permission> permissions = drive.permissions().list(fileId).execute().getItems();
        for (Permission p : permissions) {
            if (!p.getType().equals("user")) {
                continue;
            }
            if (!p.getRole().equals("writer")) {
                continue;
            }
            if (!p.getValue().equals(userEmail)) {
                continue;
            }
            drive.permissions().delete(fileId, p.getId()).execute();
        }
    }

    private static SpreadsheetEntry insertBaseData(File fileInfo, String gameId, String newTitle,
            List<String> gameInfo, List<PlayRow> plays) {
        SpreadsheetEntry spreadsheet = null;
        for (int i = 0; i < MAX_CREATION_RETRIES; ++i) {
            try {
                spreadsheet = apiUtils.getSpreadsheetByDriveFile(fileInfo);
                if (spreadsheet == null) {
                    return null;
                }
                return gameSpreadsheetCreator.populateSpreadsheet(newTitle, gameId, gameInfo, plays, spreadsheet);
            } catch (IOException exception) {
                System.err.println("I/O error while populating data for game " + gameId);
            } catch (ServiceException exception) {
                System.err.println("Google services error while populating data for game " + gameId);
            }
            if ((i + 1) < MAX_CREATION_RETRIES) {
                System.err.println("Retrying game " + gameId);
            }
        }
        return null;
    }

    private static void listAllIndexedSpreadsheets(String worksheetName) throws Exception {
        Map<String, SpreadsheetMetadata> allMetadata = spreadsheetIndexer
                .getSpreadsheetMetadataBySeason(worksheetName);
        if (allMetadata == null) {
            return;
        }
        for (Map.Entry<String, SpreadsheetMetadata> entry : allMetadata.entrySet()) {
            String urlString = "https://spreadsheets.google.com/feeds/spreadsheets/"
                    + entry.getValue().getEntryKey();
            System.out.println("\nAttempting to load entry from " + urlString);
            SpreadsheetEntry spreadsheet = spreadsheetService.getEntry(new URL(urlString), SpreadsheetEntry.class);
            System.out.println("Title: " + spreadsheet.getTitle().getPlainText());
            System.out.println("GameID: " + spreadsheet.getSummary().getPlainText());
            System.out.println("\tID: " + spreadsheet.getId());
            System.out.println("\tKey: " + spreadsheet.getKey());
            System.out.println("\tChecksum: " + entry.getValue().getChecksum());
        }
        return;
    }

    private static Set<String> makeValidCommands() {
        Set<String> s = new HashSet<String>(10);
        s.add(SET_PARENT_FOLDER_COMMAND);
        s.add(SET_DATA_DIRECTORY_COMMAND);
        s.add(ADMIN_COMMAND);
        s.add(CHARTER_COMMAND);
        s.add(GAME_COMMAND);
        s.add(FORCE_CHECKSUMS_COMMAND);
        s.add(ARCHIVE_COMMAND);
        return s;
    }
}