/*
* $Id$
*
* Copyright (c) 2000-2009 by Rodney Kinney, Brent Easton
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License (LGPL) as published by the Free Software Foundation.
*
* This library 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
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library; if not, copies are available
* at http://www.opensource.org.
*/
package VASSAL.build.module;
import java.awt.Event;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JOptionPane;
import javax.swing.KeyStroke;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import VASSAL.Info;
import VASSAL.build.Buildable;
import VASSAL.build.GameModule;
import VASSAL.build.module.metadata.AbstractMetaData;
import VASSAL.build.module.metadata.MetaDataFactory;
import VASSAL.build.module.metadata.SaveMetaData;
import VASSAL.command.Command;
import VASSAL.command.CommandEncoder;
import VASSAL.command.Logger;
import VASSAL.configure.BooleanConfigurer;
import VASSAL.configure.IconConfigurer;
import VASSAL.configure.NamedHotKeyConfigurer;
import VASSAL.i18n.Resources;
import VASSAL.launch.Launcher;
import VASSAL.tools.KeyStrokeListener;
import VASSAL.tools.NamedKeyStroke;
import VASSAL.tools.NamedKeyStrokeListener;
import VASSAL.tools.WriteErrorDialog;
import VASSAL.tools.filechooser.FileChooser;
import VASSAL.tools.filechooser.LogFileFilter;
import VASSAL.tools.io.FastByteArrayOutputStream;
import VASSAL.tools.io.FileArchive;
import VASSAL.tools.io.IOUtils;
import VASSAL.tools.io.ObfuscatingOutputStream;
import VASSAL.tools.io.ZipArchive;
import VASSAL.tools.menu.MenuManager;
import VASSAL.tools.swing.Dialogs;
public class BasicLogger implements Logger, Buildable, GameComponent, CommandEncoder {
public static final String BEGIN = "begin_log"; //$NON-NLS-1$
public static final String END = "end_log"; //$NON-NLS-1$
public static final String LOG = "LOG\t"; //$NON-NLS-1$
public static final String PROMPT_NEW_LOG = "PromptNewLog"; //$NON-NLS-1$
public static final String PROMPT_NEW_LOG_START = "PromptNewLogStart"; //$NON-NLS-1$
public static final String PROMPT_NEW_LOG_END = "PromptNewLogEnd"; //$NON-NLS-1$
public static final String PROMPT_LOG_COMMENT = "promptLogComment"; //$NON-NLS-1$
protected static final String STEP_ICON = "/images/StepForward16.gif"; //$NON-NLS-1$
protected static final String UNDO_ICON = "/images/Undo16.gif"; //$NON-NLS-1$
protected List<Command> logInput;
protected List<Command> logOutput;
protected int nextInput = 0;
protected int nextUndo = -1;
protected Command beginningState;
protected File outputFile;
protected Action stepAction = new StepAction();
protected SaveMetaData metadata;
public BasicLogger() {
super();
stepAction.setEnabled(false);
undoAction.setEnabled(false);
endLogAction.setEnabled(false);
newLogAction.setEnabled(false);
logInput = new ArrayList<Command>();
logOutput = new ArrayList<Command>();
}
public void build(Element e) { }
/**
* Expects to be added to a {@link GameModule}. Adds <code>Undo</code>,
* <code>Step</code>, and <code>End Log</code> buttons to the the control
* window toolbar. Registers {@link KeyStrokeListener}s for hotkey
* equivalents of each button.
*/
public void addTo(Buildable b) {
final GameModule mod = GameModule.getGameModule();
mod.addCommandEncoder(this);
mod.getGameState().addGameComponent(this);
final MenuManager mm = MenuManager.getInstance();
// FIMXE: setting nmemonic from first letter could cause collisions in
// some languages
newLogAction.putValue(Action.MNEMONIC_KEY,(int)Resources.getString("BasicLogger.begin_logfile.shortcut").charAt(0));
mm.addAction("BasicLogger.begin_logfile", newLogAction);
// FIMXE: setting nmemonic from first letter could cause collisions in
// some languages
endLogAction.putValue(Action.MNEMONIC_KEY,(int)Resources.getString("BasicLogger.end_logfile.shortcut").charAt(0));
mm.addAction("BasicLogger.end_logfile", endLogAction);
JButton button = mod.getToolBar().add(undoAction);
button.setToolTipText(Resources.getString("BasicLogger.undo_last_move")); //$NON-NLS-1$
button.setAlignmentY((float) 0.0);
button = mod.getToolBar().add(stepAction);
button.setToolTipText(Resources.getString("BasicLogger.step_forward_tooltip")); //$NON-NLS-1$
button.setAlignmentY((float) 0.0);
final NamedKeyStrokeListener stepKeyListener = new NamedKeyStrokeListener(stepAction, NamedKeyStroke.getNamedKeyStroke(KeyEvent.VK_PAGE_DOWN, 0));
mod.addKeyStrokeListener(stepKeyListener);
final KeyStrokeListener newLogKeyListener = new KeyStrokeListener(newLogAction, KeyStroke.getKeyStroke(KeyEvent.VK_W, Event.ALT_MASK));
mod.addKeyStrokeListener(newLogKeyListener);
final IconConfigurer stepIconConfig = new IconConfigurer("stepIcon", Resources.getString("BasicLogger.step_forward_button"), STEP_ICON); //$NON-NLS-1$ //$NON-NLS-2$
stepIconConfig.setValue(STEP_ICON);
GlobalOptions.getInstance().addOption(stepIconConfig);
stepIconConfig.addPropertyChangeListener(new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent evt) {
stepAction.putValue(Action.SMALL_ICON, stepIconConfig.getIconValue());
}
});
stepIconConfig.fireUpdate();
final IconConfigurer undoIconConfig = new IconConfigurer("undoIcon", Resources.getString("BasicLogger.undo_icon"), UNDO_ICON); //$NON-NLS-1$ //$NON-NLS-2$
undoIconConfig.setValue(UNDO_ICON);
GlobalOptions.getInstance().addOption(undoIconConfig);
undoIconConfig.addPropertyChangeListener(new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent evt) {
undoAction.putValue(Action.SMALL_ICON, undoIconConfig.getIconValue());
}
});
undoIconConfig.fireUpdate();
final NamedHotKeyConfigurer stepKeyConfig = new NamedHotKeyConfigurer("stepHotKey", Resources.getString("BasicLogger.step_forward_hotkey"), stepKeyListener.getNamedKeyStroke()); //$NON-NLS-1$ //$NON-NLS-2$
GlobalOptions.getInstance().addOption(stepKeyConfig);
stepKeyConfig.addPropertyChangeListener(new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent evt) {
stepKeyListener.setKeyStroke(stepKeyConfig.getValueNamedKeyStroke());
stepAction.putValue(Action.SHORT_DESCRIPTION, Resources.getString("BasicLogger.step_forward_tooltip2", NamedHotKeyConfigurer.getString(stepKeyListener.getKeyStroke()))); //$NON-NLS-1$
}
});
stepKeyConfig.fireUpdate();
BooleanConfigurer logOptionStart = new BooleanConfigurer(PROMPT_NEW_LOG_START, Resources.getString("BasicLogger.prompt_new_log_before"), Boolean.FALSE); //$NON-NLS-1$
mod.getPrefs().addOption(Resources.getString("Prefs.general_tab"), logOptionStart); //$NON-NLS-1$
BooleanConfigurer logOptionEnd = new BooleanConfigurer(PROMPT_NEW_LOG_END, Resources.getString("BasicLogger.prompt_new_log_after"), Boolean.TRUE); //$NON-NLS-1$
mod.getPrefs().addOption(Resources.getString("Prefs.general_tab"), logOptionEnd); //$NON-NLS-1$
BooleanConfigurer logOptionComment = new BooleanConfigurer(PROMPT_LOG_COMMENT, Resources.getString("BasicLogger.enable_comments"), Boolean.TRUE); //$NON-NLS-1$
mod.getPrefs().addOption(Resources.getString("Prefs.general_tab"), logOptionComment); //$NON-NLS-1$
}
public Element getBuildElement(Document doc) {
return doc.createElement(getClass().getName());
}
public void add(Buildable b) {
}
public void remove(Buildable b) {
}
public void setup(boolean show) {
newLogAction.setEnabled(show);
if (show) {
logOutput.clear();
nextInput = 0;
nextUndo = -1;
beginningState =
GameModule.getGameModule().getGameState().getRestoreCommand();
}
else {
if (endLogAction.isEnabled()) {
if (JOptionPane.showConfirmDialog(
GameModule.getGameModule().getFrame(),
Resources.getString("BasicLogger.save_log"), //$NON-NLS-1$
Resources.getString("BasicLogger.unsaved_log"), //$NON-NLS-1$
JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) {
try {
write();
}
catch (IOException e) {
// BasicLogger is not a lumberjack
WriteErrorDialog.error(e, outputFile);
}
}
}
logInput.clear();
beginningState = null;
undoAction.setEnabled(false);
endLogAction.setEnabled(false);
stepAction.setEnabled(false);
outputFile = null;
}
}
public boolean isLogging() {
return outputFile != null;
}
public Command getRestoreCommand() {
return null;
}
public void enableDrawing(boolean show) {
}
protected void step() {
final Command c = logInput.get(nextInput++);
c.execute();
GameModule.getGameModule().sendAndLog(c);
stepAction.setEnabled(nextInput < logInput.size());
if (!(nextInput < logInput.size())) {
queryNewLogFile(false);
}
}
/*
* Check if user would like to create a new logfile
*/
public void queryNewLogFile(boolean atStart) {
String prefName;
String prompt;
if (isLogging()) {
return;
}
if (atStart) {
prefName = PROMPT_NEW_LOG_START;
prompt = Resources.getString("BasicLogger.replay_commencing"); //$NON-NLS-1$
}
else {
prefName = PROMPT_NEW_LOG_END;
prompt = Resources.getString("BasicLogger.replay_completed"); //$NON-NLS-1$
}
final GameModule g = GameModule.getGameModule();
if (((Boolean) g.getPrefs().getValue(prefName)).booleanValue()) {
Object[] options = {
Resources.getString(Resources.YES),
Resources.getString(Resources.NO),
Resources.getString("BasicLogger.dont_prompt_again") //$NON-NLS-1$
};
int result = JOptionPane.showOptionDialog(
g.getFrame(),
Resources.getString("BasicLogger.start_new_log_file", prompt), //$NON-NLS-1$
"", //$NON-NLS-1$
JOptionPane.YES_NO_CANCEL_OPTION,
JOptionPane.QUESTION_MESSAGE,
null,
options,
options[0]
);
if (result == JOptionPane.YES_OPTION) {
beginOutput();
}
else if (result == 2) { // Turn Preference Off
g.getPrefs().setValue(prefName, Boolean.FALSE);
}
}
}
/**
* Write the logfile to a file. The file will have been selected when the logfile was begun.
*
*/
public void write() throws IOException {
if (!logOutput.isEmpty()) {
final Command log = beginningState;
for (Command c : logOutput) {
log.append(new LogCommand(c, logInput, stepAction));
}
// FIXME: Extremely inefficient! Make encode write to an OutputStream
final String s = GameModule.getGameModule().encode(log);
final FastByteArrayOutputStream ba = new FastByteArrayOutputStream();
OutputStream out = null;
try {
out = new ObfuscatingOutputStream(ba);
out.write(s.getBytes("UTF-8"));
out.close();
}
finally {
IOUtils.closeQuietly(out);
}
FileArchive archive = null;
try {
archive = new ZipArchive(outputFile);
archive.add(GameState.SAVEFILE_ZIP_ENTRY, ba.toInputStream());
metadata.save(archive);
archive.close();
}
finally {
IOUtils.closeQuietly(archive);
}
Launcher.getInstance().sendSaveCmd(outputFile);
GameModule.getGameModule().getGameState().setModified(false);
undoAction.setEnabled(false);
}
endLogAction.setEnabled(false);
}
private File getSaveFile() {
final GameModule g = GameModule.getGameModule();
final FileChooser fc = g.getFileChooser();
fc.addChoosableFileFilter(new LogFileFilter());
String name = fc.getSelectedFile() == null
? null : fc.getSelectedFile().getName();
if (name != null) {
int index = name.lastIndexOf('.');
if (index > 0) {
name = name.substring(0, index) + ".vlog"; //$NON-NLS-1$
fc.setSelectedFile(new File(fc.getSelectedFile().getParent(), name));
}
}
if (fc.showSaveDialog() != FileChooser.APPROVE_OPTION) return null;
File file = fc.getSelectedFile();
if (file.getName().indexOf('.') == -1)
file = new File(file.getParent(), file.getName() + ".vlog");
// warn user if overwriting log from an old version
if (file.exists()) {
final AbstractMetaData md = MetaDataFactory.buildMetaData(file);
if (md != null && md instanceof SaveMetaData) {
if (Info.hasOldFormat(md.getVassalVersion())) {
final int result = Dialogs.showConfirmDialog(
g.getFrame(),
Resources.getString("Warning.log_will_be_updated_title"),
Resources.getString("Warning.log_will_be_updated_heading"),
Resources.getString(
"Warning.log_will_be_updated_message",
file.getPath(),
"3.2"
),
JOptionPane.WARNING_MESSAGE,
JOptionPane.OK_CANCEL_OPTION
);
switch (result) {
case JOptionPane.CANCEL_OPTION:
case JOptionPane.CLOSED_OPTION:
return null;
}
}
}
}
return file;
}
protected void beginOutput() {
outputFile = getSaveFile();
if (outputFile == null) return;
final GameModule gm = GameModule.getGameModule();
logOutput.clear();
beginningState = gm.getGameState().getRestoreCommand();
undoAction.setEnabled(false);
endLogAction.setEnabled(true);
gm.appendToTitle(Resources.getString("BasicLogger.logging_to",
outputFile.getName()));
newLogAction.setEnabled(false);
metadata = new SaveMetaData();
}
protected void undo() {
Command lastOutput = logOutput.get(nextUndo);
Command lastInput = (nextInput > logInput.size() || nextInput < 1) ?
null : logInput.get(nextInput - 1);
if (lastInput == lastOutput) {
while (nextInput-- > 0) {
stepAction.setEnabled(true);
if (logInput.get(nextInput).getUndoCommand() != null) {
break;
}
}
}
while (nextUndo-- > 0) {
if (logOutput.get(nextUndo).getUndoCommand() != null) {
break;
}
}
undoAction.setEnabled(nextUndo >= 0);
Command undo = lastOutput.getUndoCommand();
undo.execute();
GameModule.getGameModule().getServer().sendToOthers(undo);
logOutput.add(undo);
}
public void log(Command c) {
if (c != null && c.isLoggable()) {
logOutput.add(c);
if (c.getUndoCommand() != null && !c.getUndoCommand().isNull()) {
nextUndo = logOutput.size() - 1;
}
}
undoAction.setEnabled(nextUndo >= 0);
}
/**
* Are there Input Steps yet to be replayed?
*/
public boolean hasMoreCommands() {
return nextInput < logInput.size();
}
/**
* Recognizes a logging command. The logging command is a wrapper around an ordinary {@link Command} indicating that
* the wrapped command should be stored and executed in sequence (when the <code>Step</code> button is pressed)
*/
public String encode(Command c) {
if (c instanceof LogCommand) {
return LOG + GameModule.getGameModule().encode(((LogCommand) c).getLoggedCommand());
}
else {
return null;
}
}
public Command decode(String command) {
if (command.startsWith(LOG)) {
Command logged = GameModule.getGameModule().decode(command.substring(LOG.length()));
if (logged != null) {
return new LogCommand(logged, logInput, stepAction);
}
}
return null;
}
protected Action undoAction = new UndoAction();
protected Action endLogAction = new AbstractAction(Resources.getString("BasicLogger.end_logfile")) { //$NON-NLS-1$
private static final long serialVersionUID = 1L;
public void actionPerformed(ActionEvent e) {
try {
write();
GameModule.getGameModule().warn(Resources.getString("BasicLogger.logfile_written")); //$NON-NLS-1$
newLogAction.setEnabled(true);
GameModule.getGameModule().appendToTitle(null);
outputFile = null;
}
catch (IOException ex) {
WriteErrorDialog.error(ex, outputFile);
}
}
};
protected Action newLogAction = new AbstractAction(Resources.getString("BasicLogger.begin_logfile")) { //$NON-NLS-1$
private static final long serialVersionUID = 1L;
public void actionPerformed(ActionEvent e) {
beginOutput();
}
};
public static class LogCommand extends Command {
protected Command logged;
protected List<Command> logInput;
protected Action stepAction;
public LogCommand(Command c, List<Command> logInput, Action stepAction) {
if (c instanceof LogCommand) {
throw new UnsupportedOperationException(
Resources.getString("BasicLogger.cant_log")); //$NON-NLS-1$
}
this.logInput = logInput;
this.stepAction = stepAction;
logged = c;
for (Command sub : c.getSubCommands()) {
append(new LogCommand(sub, logInput, stepAction));
}
logged.stripSubCommands();
}
protected void executeCommand() {
}
protected Command myUndoCommand() {
return null;
}
public Command getLoggedCommand() {
return logged;
}
public void execute() {
Command c = assembleCommand();
logInput.add(c);
stepAction.setEnabled(true);
}
protected Command assembleCommand() {
final Command c = logged;
for (Command sub : getSubCommands()) {
c.append(((LogCommand) sub).assembleCommand());
}
return c;
}
}
public class StepAction extends AbstractAction {
private static final long serialVersionUID = 1L;
public StepAction() {
final URL iconURL = getClass().getResource(STEP_ICON);
if (iconURL != null) {
putValue(Action.SMALL_ICON, new ImageIcon(iconURL));
}
else {
putValue(Action.NAME, Resources.getString("BasicLogger.step")); //$NON-NLS-1$
}
}
public void actionPerformed(ActionEvent e) {
step();
}
}
public class UndoAction extends AbstractAction {
private static final long serialVersionUID = 1L;
public UndoAction() {
URL iconURL = getClass().getResource(UNDO_ICON);
if (iconURL != null) {
putValue(Action.SMALL_ICON, new ImageIcon(iconURL));
}
else {
putValue(Action.NAME, Resources.getString("BasicLogger.undo")); //$NON-NLS-1$
}
}
public void actionPerformed(ActionEvent e) {
undo();
}
}
}