org.ggp.base.util.game.LocalGameRepository 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.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.util.List;
import java.util.Set;
import org.ggp.base.util.statemachine.Role;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import external.JSON.JSONException;
import external.JSON.JSONObject;
/**
* Local game repositories provide access to game resources stored on the
* local disk, bundled with the GGP Base project. For consistency with the
* web-based GGP.org infrastructure, this starts a simple HTTP server that
* provides access to the local game resources, and then uses the standard
* RemoteGameRepository interface to read from that server.
*
* @author Sam
*/
public final class LocalGameRepository extends GameRepository {
private static final int REPO_SERVER_PORT = 9140;
private static HttpServer theLocalRepoServer = null;
private static String theLocalRepoURL = "http://127.0.0.1:" + REPO_SERVER_PORT;
private static RemoteGameRepository theRealRepo;
public LocalGameRepository() {
synchronized (LocalGameRepository.class) {
if (theLocalRepoServer == null) {
try {
theLocalRepoServer = HttpServer.create(new InetSocketAddress(REPO_SERVER_PORT), 0);
theLocalRepoServer.createContext("/", new LocalRepoServer());
theLocalRepoServer.setExecutor(null); // creates a default executor
theLocalRepoServer.start();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
theRealRepo = new RemoteGameRepository(theLocalRepoURL);
}
public void cleanUp()
{
if (theLocalRepoServer != null) {
theLocalRepoServer.stop(0);
}
}
@Override
protected Game getUncachedGame(String theKey) {
return theRealRepo.getGame(theKey);
}
@Override
protected Set getUncachedGameKeys() {
return theRealRepo.getGameKeys();
}
// ========================
class LocalRepoServer implements HttpHandler {
@Override
public void handle(HttpExchange t) throws IOException {
String theURI = t.getRequestURI().toString();
byte[] response = BaseRepository.getResponseBytesForURI(theURI);
if (response == null) {
t.sendResponseHeaders(404, 0);
OutputStream os = t.getResponseBody();
os.close();
} else {
t.sendResponseHeaders(200, response.length);
OutputStream os = t.getResponseBody();
os.write(response);
os.close();
}
}
}
static class BaseRepository {
public static final String repositoryRootDirectory = theLocalRepoURL;
private BaseRepository() {
}
public static boolean shouldIgnoreFile(String fileName) {
if (fileName.startsWith(".")) return true;
if (fileName.contains(" ")) return true;
return false;
}
public static byte[] getResponseBytesForURI(String reqURI) throws IOException {
// Files not under /games/games/ aren't versioned,
// and can just be accessed directly.
if (!reqURI.startsWith("/games/")) {
return getBytesForFile(new File("games" + reqURI));
}
// Provide a listing of all of the metadata files for all of
// the games, on request.
if (reqURI.equals("/games/metadata")) {
JSONObject theGameMetaMap = new JSONObject();
for (String gameName : new File("games", "games").list()) {
if (shouldIgnoreFile(gameName)) continue;
try {
theGameMetaMap.put(gameName, new JSONObject(new String(getResponseBytesForURI("/games/" + gameName + "/"))));
} catch (JSONException e) {
e.printStackTrace();
}
}
return theGameMetaMap.toString().getBytes();
}
// Accessing the folder containing a game should show the game's
// associated metadata (which includes the contents of the folder).
if(reqURI.endsWith("/") && reqURI.length() > 9) {
reqURI += "METADATA";
}
// Extract out the version number
String thePrefix = reqURI.substring(0, reqURI.lastIndexOf("/"));
String theSuffix = reqURI.substring(reqURI.lastIndexOf("/")+1);
Integer theExplicitVersion = null;
try {
String vPart = thePrefix.substring(thePrefix.lastIndexOf("/v")+2);
theExplicitVersion = Integer.parseInt(vPart);
thePrefix = thePrefix.substring(0, thePrefix.lastIndexOf("/v"));
} catch (Exception e) {
;
}
// Sanity check: raise an exception if the parsing didn't work.
if (theExplicitVersion == null) {
if (!reqURI.equals(thePrefix + "/" + theSuffix)) {
throw new RuntimeException(reqURI + " != [~v] " + (thePrefix + "/" + theSuffix));
}
} else {
if (!reqURI.equals(thePrefix + "/v" + theExplicitVersion + "/" + theSuffix)) {
throw new RuntimeException(reqURI + " != [v] " + (thePrefix + "/v" + theExplicitVersion + "/" + theSuffix));
}
}
// When no version number is explicitly specified, assume that we want the
// latest version, whatever that is. Also, make sure the game version being
// requested actually exists (i.e. is between 0 and the max version).
int nMaxVersion = getMaxVersionForDirectory(new File("games", thePrefix));
Integer theFetchedVersion = theExplicitVersion;
if (theFetchedVersion == null) theFetchedVersion = nMaxVersion;
if (theFetchedVersion < 0 || theFetchedVersion > nMaxVersion) return null;
while (theFetchedVersion >= 0) {
byte[] theBytes = getBytesForVersionedFile(thePrefix, theFetchedVersion, theSuffix);
if (theBytes != null) {
if (theSuffix.equals("METADATA")) {
theBytes = adjustMetadataJSON(reqURI, theBytes, theExplicitVersion, nMaxVersion);
}
return theBytes;
}
theFetchedVersion--;
}
return null;
}
// When the user requests a particular version, the metadata should always be for that version.
// When the user requests the latest version, the metadata should always indicate the most recent version.
public static byte[] adjustMetadataJSON(String reqURI, byte[] theMetaBytes, Integer nExplicitVersion, int nMaxVersion) throws IOException {
try {
JSONObject theMetaJSON = new JSONObject(new String(theMetaBytes));
if (nExplicitVersion == null) {
theMetaJSON.put("version", nMaxVersion);
} else {
theMetaJSON.put("version", nExplicitVersion);
}
String theRulesheet = new String(getResponseBytesForURI(reqURI.replace("METADATA",theMetaJSON.getString("rulesheet"))));
MetadataCompleter.completeMetadataFromRulesheet(theMetaJSON, theRulesheet);
return theMetaJSON.toString().getBytes();
} catch (JSONException je) {
throw new IOException(je);
}
}
private static int getMaxVersionForDirectory(File theDir) {
if (!theDir.exists() || !theDir.isDirectory()) {
return -1;
}
int maxVersion = 0;
String[] children = theDir.list();
for (String s : children) {
if (shouldIgnoreFile(s)) continue;
if (s.startsWith("v")) {
int nVersion = Integer.parseInt(s.substring(1));
if (nVersion > maxVersion) {
maxVersion = nVersion;
}
}
}
return maxVersion;
}
private static byte[] getBytesForVersionedFile(String thePrefix, int theVersion, String theSuffix) {
if (theVersion == 0) {
return getBytesForFile(new File("games", thePrefix + "/" + theSuffix));
} else {
return getBytesForFile(new File("games", thePrefix + "/v" + theVersion + "/" + theSuffix));
}
}
private static byte[] getBytesForFile(File theFile) {
try {
if (!theFile.exists()) {
return null;
} else if (theFile.isDirectory()) {
return readDirectory(theFile).getBytes();
} else if (theFile.getName().endsWith(".png")) {
// TODO: Handle other binary formats?
return readBinaryFile(theFile);
} else if (theFile.getName().endsWith(".jpg")) {
return readBinaryFile(theFile);
} else if (theFile.getName().endsWith(".xsl")) {
return transformXSL(readFile(theFile)).getBytes();
} else if (theFile.getName().endsWith(".js")) {
return transformJS(readFile(theFile)).getBytes();
} else {
return readFile(theFile).getBytes();
}
} catch (IOException e) {
return null;
}
}
private static String transformXSL(String theContent) {
// Special case override for XSLT
return "]>\n\n" + theContent;
}
private static String transformJS(String theContent) throws IOException {
// Horrible hack; fix this later. Right now this is used to
// let games share a common board user interface, but this should
// really be handled in a cleaner, more general way with javascript
// libraries and imports.
if (theContent.contains("[BOARD_INTERFACE_JS]")) {
String theCommonBoardJS = readFile(new File("games/resources/scripts/BoardInterface.js"));
theContent = theContent.replaceFirst("\\[BOARD_INTERFACE_JS\\]", theCommonBoardJS);
}
return theContent;
}
private static String readDirectory(File theDirectory) throws IOException {
StringBuilder response = new StringBuilder();
// Show contents of the directory, using JSON notation.
response.append("[");
String[] children = theDirectory.list();
for (int i=0; i 0) {
out.write(buf);
}
in.close();
return out.toByteArray();
}
}
static class MetadataCompleter {
private MetadataCompleter() {
}
/**
* Complete fields in the metadata procedurally, based on the game rulesheet.
* This is used to fill in the number of roles, and create a list containing
* the names of all of the roles. Applications which read the game metadata
* can use these without also having to process the rulesheet.
*
* @param theMetaJSON
* @param theRulesheet
* @throws JSONException
*/
public static void completeMetadataFromRulesheet(JSONObject theMetaJSON, String theRulesheet) throws JSONException {
List theRoles = Role.computeRoles(Game.createEphemeralGame(Game.preprocessRulesheet(theRulesheet)).getRules());
theMetaJSON.put("roleNames", theRoles);
theMetaJSON.put("numRoles", theRoles.size());
}
}
}