package util.game; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStreamReader; import java.io.PrintWriter; import java.security.MessageDigest; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; import external.JSON.JSONObject; import util.configuration.ProjectConfiguration; /** * Cloud game repositories provide access to game resources stored on game * repository servers on the web, while continuing to work while the user is * offline through aggressive caching based on the immutability + versioning * scheme provided by the repository servers. * * Essentially, each game has a version number stored in the game metadata * file. Game resources are immutable until this version number changes, at * which point the game needs to be reloaded. Version numbers are passed along * and stored in the match descriptions, and repository servers will continue * to serve old versions when specifically requested, so it is valid to use any * historical game version when generating a match -- this is why we don't need * to worry about our offline cache becoming stale/invalid. However, to stay up * to date with the latest bugfixes, etc, we aggressively refresh the cache any * time we can connect to the repository server, as a matter of policy. * * Cached games are stored locally, in a directory managed by this class. These * files are compressed, to decrease their footprint on the local disk. GGP Base * has its SVN rules set up so that these caches are ignored by SVN. * * @author Sam */ public final class CloudGameRepository extends GameRepository { private final String theRepoURL; private final File theCacheDirectory; public CloudGameRepository(String theURL) { if (!theURL.startsWith("http://")) theURL = "http://" + theURL; if (theURL.endsWith("/")) theURL = theURL.substring(0, theURL.length()-1); theRepoURL = theURL; // Generate a unique hash of the repository URL, to use as the // local directory for files for the offline cache. StringBuilder theCacheHash = new StringBuilder(); try { byte[] bytesOfMessage = theRepoURL.getBytes("UTF-8"); MessageDigest md = MessageDigest.getInstance("MD5"); byte[] theDigest = md.digest(bytesOfMessage); for(int i = 0; i < theDigest.length; i++) { theCacheHash.append(Math.abs(theDigest[i])); } } catch(Exception e) { theCacheHash = null; } theCacheDirectory = new File(ProjectConfiguration.gameCacheDirectory, "repoHash" + theCacheHash); theCacheDirectory.mkdir(); // Update the game cache asynchronously. new RefreshCacheThread(theRepoURL).start(); } protected Set<String> getUncachedGameKeys() { Set<String> theKeys = new HashSet<String>(); for(File game : theCacheDirectory.listFiles()) { theKeys.add(game.getName().replace(".zip", "")); } return theKeys; } protected Game getUncachedGame(String theKey) { return loadGameFromCache(theKey); } // ================================================================ // Games are cached asynchronously in their own threads. class RefreshCacheForGameThread extends Thread { RemoteGameRepository theRepository; String theKey; public RefreshCacheForGameThread(RemoteGameRepository a, String b) { theRepository = a; theKey = b; } @Override public void run() { try { String theGameURL = theRepository.getGameURL(theKey); JSONObject theMetadata = RemoteGameRepository.getGameMetadataFromRepository(theGameURL); int repoVersion = theMetadata.getInt("version"); String versionedRepoURL = RemoteGameRepository.addVersionToGameURL(theGameURL, repoVersion); Game myGameVersion = loadGameFromCache(theKey); String myVersionedRepoURL = ""; if (myGameVersion != null) myVersionedRepoURL = myGameVersion.getRepositoryURL(); if (!versionedRepoURL.equals(myVersionedRepoURL)) { // Cache miss: we don't have the current version for // this game, and so we need to load it from the web. Game theGame = RemoteGameRepository.loadSingleGameFromMetadata(theKey, theGameURL, theMetadata); saveGameToCache(theKey, theGame); } } catch (Exception e) { e.printStackTrace(); } } } class RefreshCacheThread extends Thread { String theRepoURL; public RefreshCacheThread(String theRepoURL) { this.theRepoURL = theRepoURL; } @Override public void run() { try { // Sleep for the first two seconds after which the cache is loaded, // so that we don't interfere with the user interface startup. Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); return; } RemoteGameRepository remoteRepository = new RemoteGameRepository(theRepoURL); System.out.println("Updating the game cache..."); long beginTime = System.currentTimeMillis(); // Since games are immutable, we can guarantee that the games listed // by the repository server includes the games in the local cache, so // we can be happy just updating/refreshing the listed games. Set<String> theGameKeys = remoteRepository.getGameKeys(); if (theGameKeys == null) return; // Start threads to update every entry in the cache (or at least verify // that the entry doesn't need to be updated). Set<Thread> theThreads = new HashSet<Thread>(); for (String gameKey : theGameKeys) { Thread t = new RefreshCacheForGameThread(remoteRepository, gameKey); t.start(); theThreads.add(t); } // Wait until we've updated the cache before continuing. for (Thread t : theThreads) { try { t.join(); } catch (Exception e) { ; } } long endTime = System.currentTimeMillis(); System.out.println("Updating the game cache took: " + (endTime - beginTime) + "ms."); } } // ================================================================ private synchronized void saveGameToCache(String theKey, Game theGame) { if (theGame == null) return; File theGameFile = new File(theCacheDirectory, theKey + ".zip"); try { theGameFile.createNewFile(); FileOutputStream fOut = new FileOutputStream(theGameFile); GZIPOutputStream gOut = new GZIPOutputStream(fOut); PrintWriter pw = new PrintWriter(gOut); pw.print(theGame.serializeToJSON()); pw.flush(); pw.close(); gOut.close(); fOut.close(); } catch (Exception e) { e.printStackTrace(); } } private synchronized Game loadGameFromCache(String theKey) { File theGameFile = new File(theCacheDirectory, theKey + ".zip"); String theLine = null; try { FileInputStream fIn = new FileInputStream(theGameFile); GZIPInputStream gIn = new GZIPInputStream(fIn); InputStreamReader ir = new InputStreamReader(gIn); BufferedReader br = new BufferedReader(ir); theLine = br.readLine(); br.close(); ir.close(); gIn.close(); fIn.close(); } catch (Exception e) { ; } if (theLine == null) return null; return Game.loadFromJSON(theLine); } // ================================================================ public static void main(String[] args) { GameRepository theRepository = new CloudGameRepository("games.ggp.org"); long beginTime = System.currentTimeMillis(); Map<String, Game> theGames = new HashMap<String, Game>(); for(String gameKey : theRepository.getGameKeys()) { theGames.put(gameKey, theRepository.getGame(gameKey)); } System.out.println("Games: " + theGames.size()); long endTime = System.currentTimeMillis(); System.out.println("Time: " + (endTime - beginTime) + "ms."); } }