/* Logger - Hipbone games Copyright (C) Charles Cameron Copyright (C) 2004 John Comeau This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ package com.jcomeau.hipbone; import java.io.*; import java.awt.*; import java.net.*; import java.util.*; import java.awt.event.*; import java.util.regex.*; import com.jcomeau.*; /* this thread periodically connects to the server and checks * the Directory-Modified header; if it finds it's been updated, * it fetches what it needs to complete its internal log of the game. * * Note: yes, Directory-Modified is not a standard header, freeshell.org * was OVERWRITING my frickin standard header with Jan 1970 */ public class Logger extends Thread { URL logurl = null; Hipbone parent; // synchronize access to these fields on gameLog String lastUpdateTimestamp = null; final String RCSId = "$Id: Logger.java,v 1.31 2004/04/30 00:21:11 jcomeau Exp $"; String currentTimestamp = null; String gameLog = ""; String[] conceptions = new String[10]; boolean pleaseStop = false; // set to true when this should disappear public String[] player = {"one", "two"}; int[] playOrder = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; // 1-based // store comments for game as a whole, plus for each node (joint) String[] comments = new String[conceptions.length + 1]; // long waitinterval = 1 * 5 * 1000; // 5 seconds final String RCSID = "$Id: Logger.java,v 1.31 2004/04/30 00:21:11 jcomeau Exp $"; // Hmm, forgot that readLine() chopped newlines, have to add back in //final static String NEWLINE = System.getProperty("line.separator"); // better to set to standard newline rather than accept Windows stupid "\r\n" final static String NEWLINE = "\n"; public Logger(Hipbone parent, URL url) { this.parent = parent; logurl = url; // only allow updates to HTTP URL try { if (!url.getProtocol().equals("http")) logurl = null; } catch (Exception nullURL) {} if (logurl == null) { Common.debugprintln("local game, setting wait interval lower"); waitinterval = 100; // local game, 1/10 of a second } } public String getConception(int index) { return conceptions[index]; } public String getComment(int index) { return comments[index]; } public void setConception(int index, String text) { conceptions[index] = text; } public void setComment(int index, String text) { comments[index] = text; } public String getPlayer(int index) { return player[index % player.length]; } public void setPlayer(int index, String name) { player[index % player.length] = name; } public static void main(String args[]) { try { Logger fetcher = new Logger(null, new URL("http://jcomeau.com/hipbone/testgame/")); fetcher.sendLog("0 1 \"one\" \"test\"\n\n"); fetcher.start(); } catch (Exception exception) { Common.debugprintln("Logger failed: " + exception); } } public void run() { String log = null; Common.debugprintln("Logger " + RCSId + " starting"); while (true) { if (pleaseStop) return; // die when applet asks us to if ((!parent.stopped) && checkModification()) { Common.debugprintln("log changed, fetching and parsing"); if (logurl != null && logurl.getProtocol().equals("http")) { log = fetchlog(); Common.debugprintln("log: " + log); synchronized (gameLog) {gameLog = log;} } parseLog(); if (parent != null) parent.repaint(); } try { sleep(waitinterval); } catch (Throwable sleepException) { Common.debugprintln("cannot sleep, exiting: " + sleepException); return; } } } public boolean checkModification() { boolean modified = false; try { if (logurl != null) { HttpURLConnection connection = (HttpURLConnection)logurl.openConnection(); connection.setRequestMethod("HEAD"); connection.setUseCaches(false); connection.connect(); currentTimestamp = connection.getHeaderField("directory-modified"); Common.debugprintln("last updated: " + currentTimestamp); } if (currentTimestamp != null) { if (lastUpdateTimestamp == null || !currentTimestamp.equals(lastUpdateTimestamp)) modified = true; lastUpdateTimestamp = currentTimestamp; } } catch (Exception cannotConnect) { Common.debugprintln("problem connecting to server: " + cannotConnect); } return modified; } public String fetchlog() { String log = ""; String line = null; BufferedReader readstream; try { URL url = new URL(logurl, "?getlog"); Common.debugprintln("fetchlog starting"); URLConnection connection = url.openConnection(); Common.debugprintln("connecting to URL " + url.toString()); connection.setUseCaches(false); // must really connect to server! connection.connect(); readstream = new BufferedReader(new InputStreamReader( connection.getInputStream())); while ((line = readstream.readLine()) != null) { log += line + NEWLINE; } readstream.close(); Common.debugprintln("fetchlog: no errors"); } catch (Exception error) { Common.debugprintln("fetchlog: " + error); } return log; } public String encode(String whatever) { try { return URLEncoder.encode(whatever, "utf-8"); } catch (Exception any) { return whatever; } } public boolean sendLog(String logEntry) { BufferedReader readstream; String line; String log; Common.debugprintln("logging latest move: " + logEntry); if (logurl == null) { synchronized(gameLog) { gameLog += logEntry; currentTimestamp = new Date().toString(); } } else { try { readstream = new BufferedReader(new StringReader(logEntry)); Pattern pattern = Pattern.compile( "([0-9]+) ([0-9]+) (\"([^\"]*)\")( (\"([^\"]*)\"))?"); Matcher match = pattern.matcher(readstream.readLine()); match.find(); log = "player=" + Integer.parseInt(match.group(1)); log += "&joint=" + Integer.parseInt(match.group(2)); log += "&name=" + encode(match.group(4)); try { // this is optional, so ignore failures log += "&text=" + encode(match.group(7)); } catch (Exception noText) {} log += "&comment="; while ((line = readstream.readLine()) != null) { log += encode(line + NEWLINE); } readstream.close(); URL url = new URL(logurl, "?" + log); URLConnection connection = url.openConnection(); connection.setUseCaches(false); connection.connect(); Common.debugprintln("connected to " + url); readstream = new BufferedReader(new InputStreamReader( connection.getInputStream())); while ((line = readstream.readLine()) != null) { if (line.trim().equals("Update successful")) return true; } throw new Exception("never got 'Update successful' from CGI script"); } catch (Exception whatever) { Common.debugprintln("Failed update: " + whatever); } } return false; } public void parseLog() { String line; String state = "reading headline"; int index = 0; BufferedReader input; Common.debugprintln("parsing log into arrays"); synchronized (gameLog) { input = new BufferedReader(new StringReader(gameLog)); try { while ((line = input.readLine()) != null) { Common.debugprintln("state=" + state + ", line=" + line); if (state.equals("reading headline")) { index = readLogEntry(line); state = "reading comment"; continue; } else if (state.equals("reading comment")) { comments[index] = line + NEWLINE; } else { comments[index] += line + NEWLINE; } if (line.trim().equals("")) { state = "reading headline"; } else { state = "reading comment continuation"; } } } catch (Exception reading) { Common.debugprintln("error occurred parsing log: " + reading); } } } public int readLogEntry(String logEntry) { int player = 0; int joint = 0; String name = null; String text = null; try { Pattern pattern = Pattern.compile( "([0-9]+) ([0-9]+) (\"([^\"]*)\")( (\"([^\"]*)\"))?"); Matcher match = pattern.matcher(logEntry); match.find(); player = Integer.parseInt(match.group(1)); joint = Integer.parseInt(match.group(2)); name = match.group(4); Common.debugprintln("setting name of player " + player + " to " + name); setPlayer(player, name); if (joint > 0) { // joint number 0 indicates a comment for game as a whole try { text = match.group(7); Common.debugprintln("setting text for joint " + joint + " to " + text); setConception(joint - 1, text); updatePlayOrder(joint); } catch (Exception ignoreNoText) {} } } catch (Exception badLogEntry) { Common.debugprintln("bad log entry: " + badLogEntry); } return joint; } public void updatePlayOrder(int joint) { if (parent != null) { for (int i = 0; i < playOrder.length; i++) { if (playOrder[i] == joint) return; // don't add new entry for same if (playOrder[i] == 0) { playOrder[i] = joint; return; } } } } }