PageRenderTime 61ms CodeModel.GetById 34ms RepoModel.GetById 0ms app.codeStats 0ms

/src/org/ggp/base/util/game/LocalGameRepository.java

https://gitlab.com/mshepherd/generalgameplaying
Java | 326 lines | 245 code | 38 blank | 43 comment | 55 complexity | 677657d74437cf6531a0f371d58bf60c MD5 | raw file
  1. package org.ggp.base.util.game;
  2. import java.io.BufferedReader;
  3. import java.io.ByteArrayOutputStream;
  4. import java.io.File;
  5. import java.io.FileInputStream;
  6. import java.io.FileReader;
  7. import java.io.IOException;
  8. import java.io.InputStream;
  9. import java.io.OutputStream;
  10. import java.net.InetSocketAddress;
  11. import java.util.List;
  12. import java.util.Set;
  13. import org.ggp.base.util.statemachine.Role;
  14. import com.sun.net.httpserver.HttpExchange;
  15. import com.sun.net.httpserver.HttpHandler;
  16. import com.sun.net.httpserver.HttpServer;
  17. import external.JSON.JSONException;
  18. import external.JSON.JSONObject;
  19. /**
  20. * Local game repositories provide access to game resources stored on the
  21. * local disk, bundled with the GGP Base project. For consistency with the
  22. * web-based GGP.org infrastructure, this starts a simple HTTP server that
  23. * provides access to the local game resources, and then uses the standard
  24. * RemoteGameRepository interface to read from that server.
  25. *
  26. * @author Sam
  27. */
  28. public final class LocalGameRepository extends GameRepository {
  29. private static final int REPO_SERVER_PORT = 9140;
  30. private static HttpServer theLocalRepoServer = null;
  31. private static String theLocalRepoURL = "http://127.0.0.1:" + REPO_SERVER_PORT;
  32. private static RemoteGameRepository theRealRepo;
  33. public LocalGameRepository() {
  34. synchronized (LocalGameRepository.class) {
  35. if (theLocalRepoServer == null) {
  36. try {
  37. theLocalRepoServer = HttpServer.create(new InetSocketAddress(REPO_SERVER_PORT), 0);
  38. theLocalRepoServer.createContext("/", new LocalRepoServer());
  39. theLocalRepoServer.setExecutor(null); // creates a default executor
  40. theLocalRepoServer.start();
  41. } catch (IOException e) {
  42. throw new RuntimeException(e);
  43. }
  44. }
  45. }
  46. theRealRepo = new RemoteGameRepository(theLocalRepoURL);
  47. }
  48. public void cleanUp()
  49. {
  50. if (theLocalRepoServer != null) {
  51. theLocalRepoServer.stop(0);
  52. }
  53. }
  54. @Override
  55. protected Game getUncachedGame(String theKey) {
  56. return theRealRepo.getGame(theKey);
  57. }
  58. @Override
  59. protected Set<String> getUncachedGameKeys() {
  60. return theRealRepo.getGameKeys();
  61. }
  62. // ========================
  63. class LocalRepoServer implements HttpHandler {
  64. @Override
  65. public void handle(HttpExchange t) throws IOException {
  66. String theURI = t.getRequestURI().toString();
  67. byte[] response = BaseRepository.getResponseBytesForURI(theURI);
  68. if (response == null) {
  69. t.sendResponseHeaders(404, 0);
  70. OutputStream os = t.getResponseBody();
  71. os.close();
  72. } else {
  73. t.sendResponseHeaders(200, response.length);
  74. OutputStream os = t.getResponseBody();
  75. os.write(response);
  76. os.close();
  77. }
  78. }
  79. }
  80. static class BaseRepository {
  81. public static final String repositoryRootDirectory = theLocalRepoURL;
  82. public static boolean shouldIgnoreFile(String fileName) {
  83. if (fileName.startsWith(".")) return true;
  84. if (fileName.contains(" ")) return true;
  85. return false;
  86. }
  87. public static byte[] getResponseBytesForURI(String reqURI) throws IOException {
  88. // Files not under /games/games/ aren't versioned,
  89. // and can just be accessed directly.
  90. if (!reqURI.startsWith("/games/")) {
  91. return getBytesForFile(new File("games" + reqURI));
  92. }
  93. // Provide a listing of all of the metadata files for all of
  94. // the games, on request.
  95. if (reqURI.equals("/games/metadata")) {
  96. JSONObject theGameMetaMap = new JSONObject();
  97. for (String gameName : new File("games", "games").list()) {
  98. if (shouldIgnoreFile(gameName)) continue;
  99. try {
  100. theGameMetaMap.put(gameName, new JSONObject(new String(getResponseBytesForURI("/games/" + gameName + "/"))));
  101. } catch (JSONException e) {
  102. e.printStackTrace();
  103. }
  104. }
  105. return theGameMetaMap.toString().getBytes();
  106. }
  107. // Accessing the folder containing a game should show the game's
  108. // associated metadata (which includes the contents of the folder).
  109. if(reqURI.endsWith("/") && reqURI.length() > 9) {
  110. reqURI += "METADATA";
  111. }
  112. // Extract out the version number
  113. String thePrefix = reqURI.substring(0, reqURI.lastIndexOf("/"));
  114. String theSuffix = reqURI.substring(reqURI.lastIndexOf("/")+1);
  115. Integer theExplicitVersion = null;
  116. try {
  117. String vPart = thePrefix.substring(thePrefix.lastIndexOf("/v")+2);
  118. theExplicitVersion = Integer.parseInt(vPart);
  119. thePrefix = thePrefix.substring(0, thePrefix.lastIndexOf("/v"));
  120. } catch (Exception e) {
  121. ;
  122. }
  123. // Sanity check: raise an exception if the parsing didn't work.
  124. if (theExplicitVersion == null) {
  125. if (!reqURI.equals(thePrefix + "/" + theSuffix)) {
  126. throw new RuntimeException(reqURI + " != [~v] " + (thePrefix + "/" + theSuffix));
  127. }
  128. } else {
  129. if (!reqURI.equals(thePrefix + "/v" + theExplicitVersion + "/" + theSuffix)) {
  130. throw new RuntimeException(reqURI + " != [v] " + (thePrefix + "/v" + theExplicitVersion + "/" + theSuffix));
  131. }
  132. }
  133. // When no version number is explicitly specified, assume that we want the
  134. // latest version, whatever that is. Also, make sure the game version being
  135. // requested actually exists (i.e. is between 0 and the max version).
  136. int nMaxVersion = getMaxVersionForDirectory(new File("games", thePrefix));
  137. Integer theFetchedVersion = theExplicitVersion;
  138. if (theFetchedVersion == null) theFetchedVersion = nMaxVersion;
  139. if (theFetchedVersion < 0 || theFetchedVersion > nMaxVersion) return null;
  140. while (theFetchedVersion >= 0) {
  141. byte[] theBytes = getBytesForVersionedFile(thePrefix, theFetchedVersion, theSuffix);
  142. if (theBytes != null) {
  143. if (theSuffix.equals("METADATA")) {
  144. theBytes = adjustMetadataJSON(reqURI, theBytes, theExplicitVersion, nMaxVersion);
  145. }
  146. return theBytes;
  147. }
  148. theFetchedVersion--;
  149. }
  150. return null;
  151. }
  152. // When the user requests a particular version, the metadata should always be for that version.
  153. // When the user requests the latest version, the metadata should always indicate the most recent version.
  154. public static byte[] adjustMetadataJSON(String reqURI, byte[] theMetaBytes, Integer nExplicitVersion, int nMaxVersion) throws IOException {
  155. try {
  156. JSONObject theMetaJSON = new JSONObject(new String(theMetaBytes));
  157. if (nExplicitVersion == null) {
  158. theMetaJSON.put("version", nMaxVersion);
  159. } else {
  160. theMetaJSON.put("version", nExplicitVersion);
  161. }
  162. String theRulesheet = new String(getResponseBytesForURI(reqURI.replace("METADATA",theMetaJSON.getString("rulesheet"))));
  163. MetadataCompleter.completeMetadataFromRulesheet(theMetaJSON, theRulesheet);
  164. return theMetaJSON.toString().getBytes();
  165. } catch (JSONException je) {
  166. throw new IOException(je);
  167. }
  168. }
  169. private static int getMaxVersionForDirectory(File theDir) {
  170. if (!theDir.exists() || !theDir.isDirectory()) {
  171. return -1;
  172. }
  173. int maxVersion = 0;
  174. String[] children = theDir.list();
  175. for (String s : children) {
  176. if (shouldIgnoreFile(s)) continue;
  177. if (s.startsWith("v")) {
  178. int nVersion = Integer.parseInt(s.substring(1));
  179. if (nVersion > maxVersion) {
  180. maxVersion = nVersion;
  181. }
  182. }
  183. }
  184. return maxVersion;
  185. }
  186. private static byte[] getBytesForVersionedFile(String thePrefix, int theVersion, String theSuffix) {
  187. if (theVersion == 0) {
  188. return getBytesForFile(new File("games", thePrefix + "/" + theSuffix));
  189. } else {
  190. return getBytesForFile(new File("games", thePrefix + "/v" + theVersion + "/" + theSuffix));
  191. }
  192. }
  193. private static byte[] getBytesForFile(File theFile) {
  194. try {
  195. if (!theFile.exists()) {
  196. return null;
  197. } else if (theFile.isDirectory()) {
  198. return readDirectory(theFile).getBytes();
  199. } else if (theFile.getName().endsWith(".png")) {
  200. // TODO: Handle other binary formats?
  201. return readBinaryFile(theFile);
  202. } else if (theFile.getName().endsWith(".xsl")) {
  203. return transformXSL(readFile(theFile)).getBytes();
  204. } else if (theFile.getName().endsWith(".js")) {
  205. return transformJS(readFile(theFile)).getBytes();
  206. } else {
  207. return readFile(theFile).getBytes();
  208. }
  209. } catch (IOException e) {
  210. return null;
  211. }
  212. }
  213. private static String transformXSL(String theContent) {
  214. // Special case override for XSLT
  215. return "<!DOCTYPE stylesheet [<!ENTITY ROOT \""+repositoryRootDirectory+"\">]>\n\n" + theContent;
  216. }
  217. private static String transformJS(String theContent) throws IOException {
  218. // Horrible hack; fix this later. Right now this is used to
  219. // let games share a common board user interface, but this should
  220. // really be handled in a cleaner, more general way with javascript
  221. // libraries and imports.
  222. if (theContent.contains("[BOARD_INTERFACE_JS]")) {
  223. String theCommonBoardJS = readFile(new File("games/resources/scripts/BoardInterface.js"));
  224. theContent = theContent.replaceFirst("\\[BOARD_INTERFACE_JS\\]", theCommonBoardJS);
  225. }
  226. return theContent;
  227. }
  228. private static String readDirectory(File theDirectory) throws IOException {
  229. StringBuilder response = new StringBuilder();
  230. // Show contents of the directory, using JSON notation.
  231. response.append("[");
  232. String[] children = theDirectory.list();
  233. for (int i=0; i<children.length; i++) {
  234. if (shouldIgnoreFile(children[i])) continue;
  235. // Get filename of file or directory
  236. response.append("\"");
  237. response.append(children[i]);
  238. response.append("\",\n ");
  239. }
  240. response.delete(response.length()-3, response.length());
  241. response.append("]");
  242. return response.toString();
  243. }
  244. public static String readFile(File rootFile) throws IOException {
  245. // Show contents of the file.
  246. FileReader fr = new FileReader(rootFile);
  247. BufferedReader br = new BufferedReader(fr);
  248. try {
  249. String response = "";
  250. String line;
  251. while( (line = br.readLine()) != null ) {
  252. response += line + "\n";
  253. }
  254. return response;
  255. } finally {
  256. br.close();
  257. }
  258. }
  259. private static byte[] readBinaryFile(File rootFile) throws IOException {
  260. InputStream in = new FileInputStream(rootFile);
  261. ByteArrayOutputStream out = new ByteArrayOutputStream();
  262. // Transfer bytes from in to out
  263. byte[] buf = new byte[1024];
  264. while (in.read(buf) > 0) {
  265. out.write(buf);
  266. }
  267. in.close();
  268. return out.toByteArray();
  269. }
  270. }
  271. static class MetadataCompleter {
  272. /**
  273. * Complete fields in the metadata procedurally, based on the game rulesheet.
  274. * This is used to fill in the number of roles, and create a list containing
  275. * the names of all of the roles. Applications which read the game metadata
  276. * can use these without also having to process the rulesheet.
  277. *
  278. * @param theMetaJSON
  279. * @param theRulesheet
  280. * @throws JSONException
  281. */
  282. public static void completeMetadataFromRulesheet(JSONObject theMetaJSON, String theRulesheet) throws JSONException {
  283. List<Role> theRoles = Role.computeRoles(Game.createEphemeralGame(Game.preprocessRulesheet(theRulesheet)).getRules());
  284. theMetaJSON.put("roleNames", theRoles);
  285. theMetaJSON.put("numRoles", theRoles.size());
  286. }
  287. }
  288. }