package common; import data.AdvancedData; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.LinkedList; /** * @author Michel Bartsch * * This class should be used to log into a log file. A new file will be created * every time the GameController is started. * At the end of an actions the Log should be used to add a state into the * timeline, that is provided by this class too. * * This class is a singleton! */ public class Log { /** The instance of the singleton. */ private static Log instance = new Log(); /** The file to write into. */ private FileWriter file; /** The error-file to write into. */ private FileWriter errorFile; /** The file to write into. */ private String errorPath = "error.txt"; /** The timeline. */ private LinkedList<AdvancedData> states = new LinkedList<AdvancedData>(); /** If != null, the next log entry will use this message. */ private String message = null; /** The format of timestamps. */ public static final SimpleDateFormat timestampFormat = new SimpleDateFormat("yyyy.M.dd-kk.mm.ss"); /** * Creates a new Log. */ private Log() {} /** * Must be called once at the very beginning to allow Log to work. * * @param path The path where the log-file should be created. */ public synchronized static void init(String path) { if (instance.file != null) { throw new IllegalStateException("logger already initialized"); } try{ instance.file = new FileWriter(new File(path)); } catch (IOException e) { error("cannot write to logfile "+path); } // Write version number if application is GameController try { toFile((String)Class.forName("controller.Main").getField("version").get(null)); } catch (ClassNotFoundException ex) { } catch (NoSuchFieldException ex) { } catch (SecurityException ex) { } catch (IllegalArgumentException ex) { } catch (IllegalAccessException ex) { } } /** * Simply writes a line, beginning with a timestamp, in the file. * May be used to log something that should not be in the timeline. * * @param s The string to be written in the file. */ public static void toFile(String s) { try{ instance.file.write(timestampFormat.format(new Date(System.currentTimeMillis()))+": "+s+"\n"); instance.file.flush(); } catch (IOException e) { error("cannot write to logfile!"); } } /** * Specify the message that will be used for the next log entry. It will * replace the one that is specified during that log entry. This allows * to replace rather generic messages by more specific ones if an * action calls another action to perform its task. * @param message The message that will be used for the next log entry. */ public static void setNextMessage(String message) { instance.message = message; } /** * Puts a copy of the given data into the timeline, attaching the message * to it and writing it to the file using toFile method. * This should be used at the very end of all actions that are meant to be * in the timeline. * * @param data The current data that have just been changed and should * go into the timeline. * @param message A message describing what happened to the data. */ public static void state(AdvancedData data, String message) { AdvancedData state = (AdvancedData) data.clone(); if (instance.message == null) { state.message = message; } else { state.message = instance.message; toFile(state.message); instance.message = null; } instance.states.add(state); toFile(message); } /** * Changes the data used in all actions via the EventHandler to a data from * the timeline. So this is the undo function. * If a game state change is undone, the time when it was left is restored. * Thereby, there whole remaining log is moved into the new timeframe. * * @param states How far you want to go back, how many states. * * @return The message that was attached to the data you went back to. */ public static String goBack(int states) { if (states >= instance.states.size()) { states = instance.states.size()-1; } long laterTimestamp = instance.states.getLast().whenCurrentGameStateBegan; long earlierTimestamp = 0; long timeInCurrentState = instance.states.getLast().getTime() - laterTimestamp; for (int i=0; i<states; i++) { earlierTimestamp = instance.states.getLast().whenCurrentGameStateBegan; instance.states.removeLast(); } if (laterTimestamp != instance.states.getLast().whenCurrentGameStateBegan) { long timeOffset = laterTimestamp - earlierTimestamp + timeInCurrentState; for (AdvancedData data : instance.states) { data.whenCurrentGameStateBegan += timeOffset; } } AdvancedData state = (AdvancedData) instance.states.getLast().clone(); // Write state to EventHandler if application is GameController try { final Class<?> eventHandlerClass = Class.forName("controller.EventHandler"); eventHandlerClass.getField("data").set(eventHandlerClass.getMethod("getInstance").invoke(null), state); } catch (ClassNotFoundException ex) { } catch (NoSuchFieldException ex) { } catch (SecurityException ex) { } catch (IllegalArgumentException ex) { } catch (IllegalAccessException ex) { } catch (NoSuchMethodException ex) { } catch (InvocationTargetException ex) { } return state.message; } /** * Gives you the messages attached to the latest data in the timeline. * * @param states Of how many datas back you want to have the messages. * * @return The messages attached to the data, beginning with the latest. * The arrays length equals the states parameter. */ public static String[] getLast(int states) { String[] out = new String[states]; for (int i=0; i<states; i++) { if (instance.states.size()-1-i >= 0) { out[i] = instance.states.get(instance.states.size()-1-i).message; } else { out[i] = ""; } } return out; } /** * Writes a line, beginning with a timestamp, in the error-file and creates * a new one, if it does not yet exist. * * This can be used before initialising the log! * * @param s The string to be written in the error-file. */ public static void error(String s) { System.err.println(s); try{ if (instance.errorFile == null) { instance.errorFile = new FileWriter(new File(instance.errorPath)); } instance.errorFile.write(timestampFormat.format(new Date(System.currentTimeMillis()))+": "+s+"\n"); instance.errorFile.flush(); } catch (IOException e) { System.err.println("cannot write to error file!"); } } /** * Closes the Log * * @throws IOException if an error occurred while trying to close the FileWriters */ public static void close() throws IOException { if (instance.errorFile != null) { instance.errorFile.close(); } instance.file.close(); } }