org.ggp.base.util.game.CloudGameRepository Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of alloy-ggp-base Show documentation
Show all versions of alloy-ggp-base Show documentation
A modified version of the GGP-Base library for Alloy.
The newest version!
package org.ggp.base.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;
/**
* 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;
private static boolean needsRefresh = true;
public CloudGameRepository(String theURL) {
theRepoURL = RemoteGameRepository.properlyFormatURL(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;
}
File theCachesDirectory = new File(System.getProperty("user.home"), ".ggpserver-gamecache");
theCachesDirectory.mkdir();
theCacheDirectory = new File(theCachesDirectory, "repoHash" + theCacheHash);
if (theCacheDirectory.exists()) {
// For existing caches, only force a full refresh at most once per day
needsRefresh = (System.currentTimeMillis() - theCacheDirectory.lastModified()) > 86400000;
} else {
theCacheDirectory.mkdir();
needsRefresh = true;
}
if (needsRefresh) {
Thread refreshThread = new RefreshCacheThread(theRepoURL);
refreshThread.start();
// Update the game cache asynchronously if there are already games.
// Otherwise, force a blocking update.
if (theCacheDirectory.listFiles().length == 0) {
try {
refreshThread.join();
} catch (InterruptedException e) {
;
}
}
theCacheDirectory.setLastModified(System.currentTimeMillis());
needsRefresh = false;
}
}
@Override
protected Set getUncachedGameKeys() {
Set theKeys = new HashSet();
for(File game : theCacheDirectory.listFiles()) {
theKeys.add(game.getName().replace(".zip", ""));
}
return theKeys;
}
@Override
protected Game getUncachedGame(String theKey) {
Game cachedGame = loadGameFromCache(theKey);
if (cachedGame != null) {
return cachedGame;
}
// Request the game directly on a cache miss.
return new RemoteGameRepository(theRepoURL).getGame(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 theGameKeys = remoteRepository.getGameKeys();
if (theGameKeys == null) return;
// If the server offers a single combined metadata file, download that
// and use it to avoid checking games that haven't gotten new versions.
JSONObject bundledMetadata = remoteRepository.getBundledMetadata();
if (bundledMetadata != null) {
Set unchangedKeys = new HashSet();
for (String theKey : theGameKeys) {
try {
Game myGameVersion = loadGameFromCache(theKey);
if (myGameVersion == null)
continue;
String remoteGameURL = remoteRepository.getGameURL(theKey);
int remoteVersion = bundledMetadata.getJSONObject(theKey).getInt("version");
String remoteVersionedGameURL = RemoteGameRepository.addVersionToGameURL(remoteGameURL, remoteVersion);
// Skip updating the game cache entry if the version is the same
// and the cache entry was written less than a week ago.
if (myGameVersion.getRepositoryURL().equals(remoteVersionedGameURL) &&
getCacheEntryAge(theKey) < 604800000) {
unchangedKeys.add(theKey);
}
} catch (Exception e) {
continue;
}
}
theGameKeys.removeAll(unchangedKeys);
}
// Start threads to update every entry in the cache (or at least verify
// that the entry doesn't need to be updated).
Set theThreads = new HashSet();
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 (InterruptedException 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);
}
private synchronized long getCacheEntryAge(String theKey) {
File theGameFile = new File(theCacheDirectory, theKey + ".zip");
if (theGameFile.exists()) {
return System.currentTimeMillis() - theGameFile.lastModified();
}
return System.currentTimeMillis();
}
// ================================================================
public static void main(String[] args) {
GameRepository theRepository = new CloudGameRepository("games.ggp.org/base");
long beginTime = System.currentTimeMillis();
Map theGames = new HashMap();
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.");
}
}