/*
* $Id$
*
* Copyright (c) 2000-2003 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.preferences;
import java.io.BufferedInputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.SystemUtils;
import VASSAL.Info;
import VASSAL.build.module.WizardSupport;
import VASSAL.configure.BooleanConfigurer;
import VASSAL.configure.Configurer;
import VASSAL.configure.DirectoryConfigurer;
import VASSAL.i18n.Resources;
import VASSAL.tools.ReadErrorDialog;
import VASSAL.tools.io.IOUtils;
/**
* A set of preferences. Each set of preferences is identified by a name, and different sets may share a common editor,
* which is responsible for writing the preferences to disk
*/
public class Prefs implements Closeable {
/** Preferences key for the directory containing modules */
public static final String MODULES_DIR_KEY = "modulesDir"; // $NON_NLS-1$
public static final String DISABLE_D3D = "disableD3d";
private static Prefs globalPrefs;
private Map<String, Configurer> options = new HashMap<String, Configurer>();
private Properties storedValues = new Properties();
private PrefsEditor editor;
private File file;
public Prefs(PrefsEditor editor, String name) {
this(editor, new File(Info.getPrefsDir(), sanitize(name)));
}
protected Prefs(PrefsEditor editor, File file) {
this.editor = editor;
this.file = file;
read();
// FIXME: Use stringPropertyNames() in 1.6+
// for (String key : storedValues.stringPropertyNames()) {
for (Enumeration<?> e = storedValues.keys(); e.hasMoreElements();) {
final String key = (String) e.nextElement();
final String value = storedValues.getProperty(key);
final Configurer c = options.get(key);
if (c != null) {
c.setValue(value);
}
}
editor.addPrefs(this);
}
public PrefsEditor getEditor() {
return editor;
}
public File getFile() {
return file;
}
public void addOption(Configurer o) {
addOption(Resources.getString("Prefs.general_tab"), o); //$NON-NLS-1$
}
public void addOption(String category, Configurer o) {
addOption(category, o, null);
}
/**
* Add a configurable property to the preferences in the given category
*
* @param category
* the tab under which to add the Configurer's controls in the editor window. If null, do not add controls.
*
* @param prompt
* If non-null and the value was not read from the preferences file on initialization (i.e. first-time
* setup), prompt the user for an initial value
*/
public void addOption(String category, Configurer o, String prompt) {
if (o != null && options.get(o.getKey()) == null) {
options.put(o.getKey(), o);
final String val = storedValues.getProperty(o.getKey());
if (val != null) {
o.setValue(val);
prompt = null;
}
if (category != null && o.getControls() != null) {
editor.addOption(category, o, prompt);
}
}
}
public void setValue(String option, Object value) {
options.get(option).setValue(value);
}
public Configurer getOption(String s) {
return options.get(s);
}
/**
* @param key
* @return the value of the preferences setting stored under key
*/
public Object getValue(String key) {
final Configurer c = options.get(key);
return c == null ? null : c.getValue();
}
/**
* Return the value of a given preference.
*
* @param key
* the name of the preference to retrieve
* @return the value of this option read from the Preferences file at startup, or <code>null</code> if no value is
* undefined
*/
public String getStoredValue(String key) {
return storedValues.getProperty(key);
}
public static String sanitize(String str) {
/*
Java gives us no way of checking whether a string is a valid
filename on the filesystem we're using. Filenames matching
[0-9A-Za-z_]+ are safe pretty much everywhere. Any code point
in [0-9A-Za-z] is passed through; every other code point c is
escaped as "_hex(c)_". This mapping is a surjection and will
produce filenames safe on every sane filesystem, so long as the
input strings are not too long.
*/
final StringBuilder sb = new StringBuilder();
for (int i = 0; i < str.length(); ++i) {
final int cp = str.codePointAt(i);
if (('0' <= cp && cp <= '9') ||
('A' <= cp && cp <= 'Z') ||
('a' <= cp && cp <= 'z')) {
sb.append((char) cp);
}
else {
sb.append('_')
.append(Integer.toHexString(cp).toUpperCase())
.append('_');
}
}
return sb.toString();
}
protected void read() {
InputStream in = null;
try {
in = new BufferedInputStream(new FileInputStream(file));
storedValues.clear();
storedValues.load(in);
in.close();
}
catch (FileNotFoundException e) {
// First time for this module, not an error.
}
catch (IOException e) {
ReadErrorDialog.errorNoI18N(e, file);
}
finally {
IOUtils.closeQuietly(in);
}
}
/**
* Store this set of preferences in the editor, but don't yet save to disk
*/
public void save() throws IOException {
storedValues.clear();
// ensure that the prefs dir exists
if (!Info.getPrefsDir().exists()) {
FileUtils.forceMkdir(Info.getPrefsDir());
}
RandomAccessFile raf = null;
try {
raf = new RandomAccessFile(file, "rw");
final FileChannel ch = raf.getChannel();
// lock the prefs file
final FileLock lock = ch.lock();
// read the old key-value pairs
final InputStream in = Channels.newInputStream(ch);
storedValues.load(in);
// merge in the current key-value pairs
for (Configurer c : options.values()) {
final String val = c.getValueString();
if (val != null) {
storedValues.put(c.getKey(), val);
}
}
// write back the key-value pairs
ch.truncate(0);
ch.position(0);
final OutputStream out = Channels.newOutputStream(ch);
storedValues.store(out, null);
out.flush();
}
finally {
// also closes the channel, the streams, and releases the lock
IOUtils.closeQuietly(raf);
}
}
/** Save these preferences and write to disk. */
public void write() throws IOException {
save();
}
public void close() throws IOException {
save();
if (this == globalPrefs) {
globalPrefs = null;
}
}
/**
* A global set of preferences that exists independent of any individual module.
*
* @return the global <code>Prefs</code> object
*/
public static Prefs getGlobalPrefs() {
if (globalPrefs == null) {
final PrefsEditor ed = new PrefsEditor();
// The underscore prevents collisions with module prefs
globalPrefs = new Prefs(ed, new File(Info.getPrefsDir(), "V_Global"));
final DirectoryConfigurer c =
new DirectoryConfigurer(MODULES_DIR_KEY, null);
c.setValue(new File(System.getProperty("user.home")));
globalPrefs.addOption(null, c);
}
return globalPrefs;
}
/**
* Initialize visible Global Preferences that are shared between the
* Module Manager and the Editor/Player.
*
*/
public static void initSharedGlobalPrefs() {
getGlobalPrefs();
// Option to disable D3D pipeline
if (SystemUtils.IS_OS_WINDOWS) {
final BooleanConfigurer d3dConf = new BooleanConfigurer(
DISABLE_D3D,
Resources.getString("Prefs.disable_d3d"),
Boolean.FALSE
);
globalPrefs.addOption(d3dConf);
}
final BooleanConfigurer wizardConf = new BooleanConfigurer(
WizardSupport.WELCOME_WIZARD_KEY,
Resources.getString("WizardSupport.ShowWizard"),
Boolean.TRUE
);
globalPrefs.addOption(wizardConf);
}
}