/*
* $Id$
*
* Copyright (c) 2000-2007 by Rodney Kinney
*
* 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.documentation;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import javax.swing.AbstractAction;
import javax.swing.Action;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import VASSAL.build.AbstractBuildable;
import VASSAL.build.AutoConfigurable;
import VASSAL.build.Buildable;
import VASSAL.build.Configurable;
import VASSAL.build.GameModule;
import VASSAL.configure.AutoConfigurer;
import VASSAL.configure.Configurer;
import VASSAL.configure.ConfigurerFactory;
import VASSAL.configure.DirectoryConfigurer;
import VASSAL.configure.VisibilityCondition;
import VASSAL.i18n.ComponentI18nData;
import VASSAL.tools.BrowserSupport;
import VASSAL.tools.WriteErrorDialog;
import VASSAL.tools.io.IOUtils;
import VASSAL.tools.menu.MenuItemProxy;
import VASSAL.tools.menu.MenuManager;
/**
* Unpacks a zipped directory stored in the module and displays it in an
* external browser window.
*
* @author rkinney
*/
public class BrowserHelpFile extends AbstractBuildable implements Configurable {
private static final Logger logger =
LoggerFactory.getLogger(BrowserHelpFile.class);
public static final String TITLE = "title"; //$NON-NLS-1$
public static final String CONTENTS = "contents"; //$NON-NLS-1$
public static final String STARTING_PAGE = "startingPage"; //$NON-NLS-1$
protected String name;
protected String startingPage;
protected Action launch;
protected URL url;
protected PropertyChangeSupport propSupport = new PropertyChangeSupport(this);
protected ComponentI18nData myI18nData;
public BrowserHelpFile() {
super();
launch = new AbstractAction() {
private static final long serialVersionUID = 1L;
public void actionPerformed(ActionEvent e) {
launch();
}
};
}
public void launch() {
if (url == null) {
extractContents();
}
if (url != null) {
BrowserSupport.openURL(url.toString());
}
}
/**
* The entry in the module Zip file containing the HTML directory
* @return
*/
protected String getContentsResource() {
return name == null ? null : name.replace(' ', '_');
}
protected void extractContents() {
ZipInputStream in = null;
try {
try {
in = new ZipInputStream(new BufferedInputStream(
GameModule.getGameModule().getDataArchive()
.getInputStream("help/" + getContentsResource()))); //$NON-NLS-1$
}
catch (IOException e) {
// The help file was created with empty contents.
// Assume an absolute URL as the starting page.
url = new URL(startingPage);
return;
}
final File tmp = File.createTempFile("VASSAL", "help"); //$NON-NLS-1$ //$NON-NLS-2$
File output = tmp.getParentFile();
tmp.delete();
output = new File(output, "VASSAL"); //$NON-NLS-1$
output = new File(output, "help"); //$NON-NLS-1$
output = new File(output, getContentsResource());
if (output.exists()) recursiveDelete(output);
output.mkdirs();
ZipEntry entry;
while ((entry = in.getNextEntry()) != null) {
if (entry.isDirectory()) {
new File(output, entry.getName()).mkdirs();
}
else {
// FIXME: no way to distinguish between read and write errors here
FileOutputStream fos = null;
try {
fos = new FileOutputStream(new File(output, entry.getName()));
IOUtils.copy(in, fos);
fos.close();
}
finally {
IOUtils.closeQuietly(fos);
}
}
}
in.close();
url = new File(output, startingPage).toURI().toURL();
}
// FIXME: review error message
catch (IOException e) {
logger.error("", e);
}
finally {
IOUtils.closeQuietly(in);
}
}
protected void recursiveDelete(File output) {
if (output.isDirectory()) {
for (File f : output.listFiles()) {
recursiveDelete(f);
}
}
else {
output.delete();
}
}
public String[] getAttributeNames() {
return new String[]{
TITLE,
CONTENTS,
STARTING_PAGE
};
}
public String getAttributeValueString(String key) {
if (TITLE.equals(key)) {
return name;
}
else if (STARTING_PAGE.equals(key)) {
return startingPage;
}
return null;
}
public void setAttribute(String key, Object value) {
if (TITLE.equals(key)) {
name = (String) value;
launch.putValue(Action.NAME, name);
url = null;
getI18nData().setUntranslatedValue(key, name);
}
else if (STARTING_PAGE.equals(key)) {
startingPage = (String) value;
url = null;
}
}
protected MenuItemProxy launchItem;
public void addTo(Buildable parent) {
launchItem = new MenuItemProxy(launch);
MenuManager.getInstance().addToSection("Documentation.Module", launchItem);
launch.setEnabled(true);
}
public void removeFrom(Buildable parent) {
MenuManager.getInstance()
.removeFromSection("Documentation.Module", launchItem);
launch.setEnabled(false);
}
public void addPropertyChangeListener(PropertyChangeListener l) {
propSupport.addPropertyChangeListener(l);
}
public Class<?>[] getAllowableConfigureComponents() {
return new Class<?>[0];
}
public Configurable[] getConfigureComponents() {
return new Configurable[0];
}
public String getConfigureName() {
return name;
}
public Configurer getConfigurer() {
return new MyConfigurer(new ConfigSupport());
}
public HelpFile getHelpFile() {
return HelpFile.getReferenceManualPage("HelpMenu.htm","HtmlHelpFile"); //$NON-NLS-1$ //$NON-NLS-2$
}
public void remove(Buildable child) {
}
public static String getConfigureTypeName() {
return "HTML Help File"; //$NON-NLS-1$
}
/**
* The attributes we want to expose in the editor are not the same as the ones we want to save to the buildFile, so we
* use this object to specify the properties in the editor. Also packs up the contents directory and saves it to the
* ArchiveWriter
*/
protected class ConfigSupport implements AutoConfigurable {
public static final String DIR = "dir"; //$NON-NLS-1$
protected File dir;
public String[] getAttributeDescriptions() {
return new String[]{
"Menu Entry: ",
"Contents: ",
"Starting Page: "
};
}
public String[] getAttributeNames() {
return new String[]{
TITLE, DIR,
STARTING_PAGE
};
}
public Class<?>[] getAttributeTypes() {
return new Class<?>[]{
String.class,
ContentsConfig.class,
String.class
};
}
public String getAttributeValueString(String key) {
if (DIR.equals(key)) {
return dir == null ? null : dir.getPath();
}
else {
return BrowserHelpFile.this.getAttributeValueString(key);
}
}
public VisibilityCondition getAttributeVisibility(String name) {
return null;
}
public void setAttribute(String key, Object value) {
if (DIR.equals(key)) {
dir = (File) value;
}
else {
BrowserHelpFile.this.setAttribute(key, value);
}
}
public void packContents() {
if (dir == null) return;
File packed = null;
try {
packed = File.createTempFile("VASSALhelp", ".zip"); //$NON-NLS-1$ //$NON-NLS-2$
ZipOutputStream out = null;
try {
out = new ZipOutputStream(new FileOutputStream(packed));
for (File f : dir.listFiles()) {
packFile(f, "", out); //$NON-NLS-1$
}
out.close();
}
finally {
IOUtils.closeQuietly(out);
}
GameModule.getGameModule().getArchiveWriter().addFile(
packed.getPath(),
"help/" + BrowserHelpFile.this.getContentsResource()); //$NON-NLS-1$
}
catch (IOException e) {
WriteErrorDialog.error(e, packed);
}
}
protected void packFile(File packed, String prefix, ZipOutputStream out)
throws IOException {
if (packed.isDirectory()) {
for (File f : packed.listFiles()) {
packFile(f, prefix + packed.getName() + "/", out); //$NON-NLS-1$
}
}
else {
final ZipEntry entry = new ZipEntry(prefix + packed.getName());
out.putNextEntry(entry);
FileInputStream in = null;
try {
in = new FileInputStream(packed);
IOUtils.copy(in, out);
in.close();
}
finally {
IOUtils.closeQuietly(in);
}
}
}
public void addPropertyChangeListener(PropertyChangeListener l) {
BrowserHelpFile.this.addPropertyChangeListener(l);
}
public Class<?>[] getAllowableConfigureComponents() {
return BrowserHelpFile.this.getAllowableConfigureComponents();
}
public Configurable[] getConfigureComponents() {
return BrowserHelpFile.this.getConfigureComponents();
}
public String getConfigureName() {
return BrowserHelpFile.this.getConfigureName();
}
public VASSAL.configure.Configurer getConfigurer() {
return null;
}
public HelpFile getHelpFile() {
return BrowserHelpFile.this.getHelpFile();
}
public void remove(Buildable child) {
}
public void removeFrom(Buildable parent) {
}
public void add(Buildable child) {
}
public void addTo(Buildable parent) {
}
public void build(Element e) {
}
public Element getBuildElement(Document doc) {
return null;
}
public ComponentI18nData getI18nData() {
return null;
}
}
public static class ContentsConfig implements ConfigurerFactory {
public Configurer getConfigurer(AutoConfigurable c, String key, String name) {
return new DirectoryConfigurer(key, name){
public Component getControls() {
Component controls = super.getControls();
tf.setEditable(false);
return controls;
}
};
}
}
/**
* Handles the packaging of the target directory into the module file after the user saves the properties in the
* editor
*
* @author rkinney
*
*/
protected static class MyConfigurer extends AutoConfigurer {
public MyConfigurer(AutoConfigurable c) {
super(c);
}
public Object getValue() {
if (target != null) {
((ConfigSupport)target).packContents();
}
return super.getValue();
}
}
public ComponentI18nData getI18nData() {
if (myI18nData == null) {
myI18nData = new ComponentI18nData(this, "BrowserHelpFile." + getConfigureName(), null,
new String[] {TITLE},
new boolean[] {true},
new String[] {"Menu Entry: "});
}
return myI18nData;
}
}