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<String> 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 "<!DOCTYPE stylesheet [<!ENTITY ROOT \""+repositoryRootDirectory+"\">]>\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<children.length; i++) { if (shouldIgnoreFile(children[i])) continue; // Get filename of file or directory response.append("\""); response.append(children[i]); response.append("\",\n "); } response.delete(response.length()-3, response.length()); response.append("]"); return response.toString(); } public static String readFile(File rootFile) throws IOException { // Show contents of the file. FileReader fr = new FileReader(rootFile); BufferedReader br = new BufferedReader(fr); try { String response = ""; String line; while( (line = br.readLine()) != null ) { response += line + "\n"; } return response; } finally { br.close(); } } private static byte[] readBinaryFile(File rootFile) throws IOException { InputStream in = new FileInputStream(rootFile); ByteArrayOutputStream out = new ByteArrayOutputStream(); // Transfer bytes from in to out byte[] buf = new byte[1024]; while (in.read(buf) > 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<Role> theRoles = Role.computeRoles(Game.createEphemeralGame(Game.preprocessRulesheet(theRulesheet)).getRules()); theMetaJSON.put("roleNames", theRoles); theMetaJSON.put("numRoles", theRoles.size()); } } }