package ecologylab.oodss.logging.playback; import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.WindowEvent; import java.awt.event.WindowListener; import java.io.File; import java.util.LinkedList; import java.util.List; import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.Timer; import javax.swing.UIManager; import javax.swing.UnsupportedLookAndFeelException; import ecologylab.appframework.PropertiesAndDirectories; import ecologylab.appframework.SingletonApplicationEnvironment; import ecologylab.oodss.logging.Logging; import ecologylab.oodss.logging.MixedInitiativeOp; import ecologylab.oodss.logging.translationScope.MixedInitiativeOpClassesProvider; import ecologylab.serialization.SIMPLTranslationException; import ecologylab.serialization.SimplTypesScope; /** * The main application for playing back log files. * * @author Zachary O. Toups (zach@ecologylab.net) */ public abstract class LogPlayer<OP extends MixedInitiativeOp, LOG extends Logging<OP>> extends SingletonApplicationEnvironment implements ActionListener, WindowListener, PlaybackControlCommands, Runnable { private static final int LOG_LOADED = 2; private static final int LOG_LOADING = 1; private static final int LOG_NOT_SELECTED = 0; private List<ActionListener> actionListeners = new LinkedList<ActionListener>(); protected LogPlaybackControls<OP, LOG> controlsDisplay = null; protected View<OP> logDisplay = null; protected LogPlaybackControlModel<OP, LOG> log; protected File logFile = null; protected JFrame mainFrame; protected boolean playing = false; protected Timer t; /** * Modes: 0 = no file selected (need to get a file name). 1 = file selected and loading. 2 = file * loaded, display log. */ protected int mode = LOG_NOT_SELECTED; private boolean guiShown; protected SimplTypesScope translationScope; public final static int DEFAULT_PLAYBACK_INTERVAL = 100; public final static int TIMESTAMP_PLAYBACK_INTERVAL = -1; /** * The number of milliseconds between ops when the log is playing. Setting to -1 will try to use * op timestamps. */ protected int playbackInterval = DEFAULT_PLAYBACK_INTERVAL; private boolean logLoadComplete = false; /** * * @param appName * @param args * @param translationScope * @param opSubclasses * An array of subclasses of MixedInitiativeOp that will be used to translate the * operations read in by the player. * @throws SIMPLTranslationException */ public LogPlayer(String appName, String[] args, SimplTypesScope translationScope, Class[] opSubclasses) throws SIMPLTranslationException { super(appName, translationScope, (SimplTypesScope) null, args, 0); // create a translation scope for the opSubclasses if (opSubclasses == null) opSubclasses = MixedInitiativeOpClassesProvider.STATIC_INSTANCE.provideClasses(); SimplTypesScope.get(Logging.MIXED_INITIATIVE_OP_TRANSLATION_SCOPE, opSubclasses); guiShown = false; if (translationScope != null) this.translationScope = translationScope; else this.translationScope = SimplTypesScope.get(Logging.MIXED_INITIATIVE_OP_TRANSLATION_SCOPE); LOG incomingLog = null; // see if a log file has been specified if (args.length > 1) { // a log was specified debug("Getting log file from args!"); logFile = new File(args[1]); mode = LOG_LOADING; logLoadComplete = false; } while (incomingLog == null) { // Schedule a job for the event-dispatching thread: // creating and showing this application's GUI. javax.swing.SwingUtilities.invokeLater(this); while (mode != LOG_LOADING || logFile == null) { // wait until we get a logFile or until the program quits try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } // by now, incomingLog must have something debug("Reading log from file: " + logFile); // Schedule a job for the event-dispatching thread: // creating and showing this application's GUI. javax.swing.SwingUtilities.invokeLater(this); incomingLog = this.readInXMLFile(logFile); if (incomingLog != null) { this.log = generateLogPlaybackControlModel(incomingLog); t = new Timer(this.playbackInterval, this); mode = LOG_LOADED; secondaryLoad(); } else { System.err.println("No log found; exiting."); // System.exit(0); mode = LOG_NOT_SELECTED; logLoadComplete = false; } // Schedule a job for the event-dispatching thread: // creating and showing this application's GUI. javax.swing.SwingUtilities.invokeLater(this); } } protected abstract LogPlaybackControlModel<OP, LOG> generateLogPlaybackControlModel( LOG incomingLog); protected abstract View<OP> generateView(); protected abstract LogPlaybackControls<OP, LOG> generateLogPlaybackControls(); /** * Translates the given file to XML. Necessary because it is not possible to cast directly to * generic types. * * @param logToRead * @return * @throws SIMPLTranslationException */ protected abstract LOG translateXMLFromFile(File logToRead) throws SIMPLTranslationException; protected LOG readInXMLFile(File logToRead) { if (logToRead == null) { System.err.println("NO LOG TO READ!"); return null; } // read in the XML; this may take awhile try { debug("READING LOG"); return translateXMLFromFile(logToRead); } catch (SIMPLTranslationException e) { System.err.println("READING LOG FAILED!"); e.printStackTrace(); return null; } } protected JFileChooser createFileChooser() { JFileChooser fC = new JFileChooser(PropertiesAndDirectories.logDir()); ExtensionFilter fF = new ExtensionFilter("xml"); fC.addChoosableFileFilter(fC.getAcceptAllFileFilter()); fC.addChoosableFileFilter(fF); // find the most recent log file in the directory File[] logDirContents = PropertiesAndDirectories.logDir().listFiles(fF); if (logDirContents.length > 0) { File newestLog = logDirContents[0]; for (int i = 1; i < logDirContents.length; i++) { if (logDirContents[i].lastModified() > newestLog.lastModified()) { newestLog = logDirContents[i]; } } fC.setSelectedFile(newestLog); } return fC; } @Override public void actionPerformed(ActionEvent e) { if (e.getActionCommand() == null) { if (playing && !this.controlsDisplay.isMousePressed()) { // advance the frame log.forward(); if (this.playbackInterval == TIMESTAMP_PLAYBACK_INTERVAL) { // if we're basing off the interval, we have to check some // stuff long delay = log.getNext().getSessionTime() - log.getCurrentOp().getSessionTime(); if (delay > 0) { t.setDelay((int) delay); } } if (log.getMaximum() <= log.getValue()) { this.fireActionEvent(new ActionEvent(this, 0, PAUSE)); } } } if (PLAY.equals(e.getActionCommand())) { playing = true; } if (PAUSE.equals(e.getActionCommand())) { playing = false; } if (STOP.equals(e.getActionCommand())) { playing = false; log.reset(); } if (STEP_BACK.equals(e.getActionCommand())) { log.back(); } if (STEP_FORWARD.equals(e.getActionCommand())) { log.forward(); } if (logDisplay != null) logDisplay.changeOp(log.getCurrentOp()); mainFrame.repaint(); } public void addActionListener(ActionListener l) { actionListeners.add(l); } protected void fireActionEvent(ActionEvent e) { for (ActionListener l : actionListeners) { l.actionPerformed(e); } } protected void showLogPlaybackGUI() { if (logDisplay != null) logDisplay.load(this, log, log.getLogPrologue()); controlsDisplay = generateLogPlaybackControls(); if (controlsDisplay != null) { controlsDisplay.setPreferredSize(new Dimension(800, 100)); controlsDisplay.setMinimumSize(new Dimension(800, 100)); controlsDisplay.setLoading(true); controlsDisplay.setLog(log); controlsDisplay.setLoading(false); controlsDisplay.setupImportantEvents(); } // logDisplay.setPreferredSize(new Dimension(800, 600)); // logDisplay.setMinimumSize(new Dimension(800, 600)); // logDisplay.setMaximumSize(new Dimension(800, 600)); if (logDisplay != null) logDisplay.invalidate(); mainFrame.getContentPane().removeAll(); if (logDisplay != null) mainFrame.getContentPane().add(logDisplay, BorderLayout.CENTER); if (controlsDisplay != null) mainFrame.getContentPane().add(controlsDisplay, BorderLayout.SOUTH); if (logDisplay != null && logDisplay.hasKeyListenerSubObject()) { mainFrame.addKeyListener(logDisplay.getKeyListenerSubObject()); } if (logDisplay != null && logDisplay.hasActionListenerSubObject()) { t.addActionListener(logDisplay.getActionListenerSubObject()); } mainFrame.invalidate(); mainFrame.pack(); mainFrame.setVisible(true); logLoadComplete = true; } private void showLoadingGUI() { // logDisplay.setPreferredSize(new Dimension(800, 600)); if (logDisplay != null) logDisplay.invalidate(); } /** * Creates the GUI for the log playback application. */ private void createGUI() { // JFrame.setDefaultLookAndFeelDecorated(true); try { // Set cross-platform Java L&F (also called "Metal") UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName()); } catch (UnsupportedLookAndFeelException e) { // handle exception e.printStackTrace(); } catch (ClassNotFoundException e) { // handle exception e.printStackTrace(); } catch (InstantiationException e) { // handle exception e.printStackTrace(); } catch (IllegalAccessException e) { // handle exception e.printStackTrace(); } mainFrame = new JFrame(PropertiesAndDirectories.applicationName()); mainFrame.addWindowListener(this); mainFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // mainFrame.setBackground(Color.BLACK); mainFrame.setPreferredSize(new Dimension(825, 875)); mainFrame.setFocusable(true); mainFrame.requestFocus(); mainFrame.getContentPane().setLayout(new BorderLayout()); logDisplay = this.generateView(); // mainFrame.getContentPane().add(logDisplay); mainFrame.validate(); mainFrame.pack(); mainFrame.setVisible(true); } public void selectFile() { showFileSelectGUI(); mode = LOG_LOADING; javax.swing.SwingUtilities.invokeLater(this); } public void showFileSelectGUI() { JFileChooser fC = this.createFileChooser(); int returnVal = fC.showOpenDialog(mainFrame); if (returnVal == JFileChooser.APPROVE_OPTION) { // we now have a file...display a new GUI logFile = fC.getSelectedFile(); } else { // told to cancel; quitting debug("No log selected; quitting."); System.exit(0); } } protected void secondaryLoad() { } public void startAdjusting() { } /** * Used to be thread safe with Swing. Either sets up the UI, or switches it, depending on whether * or not a file has been selected. * * @see java.lang.Runnable#run() */ @Override public void run() { if (!guiShown) { debug("Showing GUI."); this.createGUI(); guiShown = true; } if (!logLoadComplete) { debug("mode is: " + mode); switch (mode) { case (LOG_NOT_SELECTED): // no file selected this.selectFile(); break; case (LOG_LOADING): this.showLoadingGUI(); break; case (LOG_LOADED): this.showLogPlaybackGUI(); break; } } } public void startProgram() { // Schedule a job for the event-dispatching thread: // creating and showing this application's GUI. javax.swing.SwingUtilities.invokeLater(this); t.start(); } @Override public void windowActivated(WindowEvent e) { } @Override public void windowClosed(WindowEvent e) { t.stop(); } /** * Sends the server notification that we are logging-out, then shuts down the program. */ @Override public void windowClosing(WindowEvent e) { } @Override public void windowDeactivated(WindowEvent e) { } @Override public void windowDeiconified(WindowEvent e) { t.start(); } @Override public void windowIconified(WindowEvent e) { t.stop(); } @Override public void windowOpened(WindowEvent e) { } }