// License: GPL. Copyright 2007 by Immanuel Scholz and others
package org.openstreetmap.josm.data;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.awt.Color;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.Map.Entry;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.JOptionPane;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.tools.ColorHelper;
/**
* This class holds all preferences for JOSM.
*
* Other classes can register their beloved properties here. All properties will be
* saved upon set-access.
*
* @author imi
*/
public class Preferences {
//static private final Logger logger = Logger.getLogger(Preferences.class.getName());
/**
* Internal storage for the preference directory.
* Do not access this variable directly!
* @see #getPreferencesDirFile()
*/
private File preferencesDirFile = null;
public interface PreferenceChangeEvent{
String getKey();
String getOldValue();
String getNewValue();
}
public interface PreferenceChangedListener {
void preferenceChanged(PreferenceChangeEvent e);
}
private static class DefaultPreferenceChangeEvent implements PreferenceChangeEvent {
private final String key;
private final String oldValue;
private final String newValue;
public DefaultPreferenceChangeEvent(String key, String oldValue, String newValue) {
this.key = key;
this.oldValue = oldValue;
this.newValue = newValue;
}
public String getKey() {
return key;
}
public String getOldValue() {
return oldValue;
}
public String getNewValue() {
return newValue;
}
}
/**
* Class holding one bookmarkentry.
* @author imi
*/
public static class Bookmark implements Comparable<Bookmark> {
private String name;
private Bounds area;
public Bookmark() {
area = null;
name = null;
}
public Bookmark(Bounds area) {
this.area = area;
}
@Override public String toString() {
return name;
}
public int compareTo(Bookmark b) {
return name.toLowerCase().compareTo(b.name.toLowerCase());
}
public Bounds getArea() {
return area;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void setArea(Bounds area) {
this.area = area;
}
}
public interface ColorKey {
String getColorName();
String getSpecialName();
Color getDefault();
}
private final CopyOnWriteArrayList<PreferenceChangedListener> listeners = new CopyOnWriteArrayList<PreferenceChangedListener>();
public void addPreferenceChangeListener(PreferenceChangedListener listener) {
if (listener != null) {
listeners.addIfAbsent(listener);
}
}
public void removePreferenceChangeListener(PreferenceChangedListener listener) {
listeners.remove(listener);
}
protected void firePrefrenceChanged(String key, String oldValue, String newValue) {
PreferenceChangeEvent evt = new DefaultPreferenceChangeEvent(key, oldValue, newValue);
for (PreferenceChangedListener l : listeners) {
l.preferenceChanged(evt);
}
}
/**
* Map the property name to the property object.
*/
protected final SortedMap<String, String> properties = new TreeMap<String, String>();
protected final SortedMap<String, String> defaults = new TreeMap<String, String>();
/**
* Override some values on read. This is intended to be used for technology previews
* where we want to temporarily modify things without changing the user's preferences
* file.
*/
protected static final SortedMap<String, String> override = new TreeMap<String, String>();
static {
//override.put("osm-server.version", "0.5");
//override.put("osm-server.additional-versions", "");
//override.put("osm-server.url", "http://openstreetmap.gryph.de/api");
//override.put("plugins", null);
}
/**
* Return the location of the user defined preferences file
*/
public String getPreferencesDir() {
final String path = getPreferencesDirFile().getPath();
if (path.endsWith(File.separator))
return path;
return path + File.separator;
}
public File getPreferencesDirFile() {
if (preferencesDirFile != null)
return preferencesDirFile;
String path;
path = System.getProperty("josm.home");
if (path != null) {
preferencesDirFile = new File(path);
} else {
path = System.getenv("APPDATA");
if (path != null) {
preferencesDirFile = new File(path, "JOSM");
} else {
preferencesDirFile = new File(System.getProperty("user.home"), ".josm");
}
}
return preferencesDirFile;
}
public File getPreferenceFile() {
return new File(getPreferencesDirFile(), "preferences");
}
public File getPluginsDirectory() {
return new File(getPreferencesDirFile(), "plugins");
}
/**
* @return A list of all existing directories where resources could be stored.
*/
public Collection<String> getAllPossiblePreferenceDirs() {
LinkedList<String> locations = new LinkedList<String>();
locations.add(Main.pref.getPreferencesDir());
String s;
if ((s = System.getenv("JOSM_RESOURCES")) != null) {
if (!s.endsWith(File.separator)) {
s = s + File.separator;
}
locations.add(s);
}
if ((s = System.getProperty("josm.resources")) != null) {
if (!s.endsWith(File.separator)) {
s = s + File.separator;
}
locations.add(s);
}
String appdata = System.getenv("APPDATA");
if (System.getenv("ALLUSERSPROFILE") != null && appdata != null
&& appdata.lastIndexOf(File.separator) != -1) {
appdata = appdata.substring(appdata.lastIndexOf(File.separator));
locations.add(new File(new File(System.getenv("ALLUSERSPROFILE"),
appdata), "JOSM").getPath());
}
locations.add("/usr/local/share/josm/");
locations.add("/usr/local/lib/josm/");
locations.add("/usr/share/josm/");
locations.add("/usr/lib/josm/");
return locations;
}
synchronized public boolean hasKey(final String key) {
return override.containsKey(key) ? override.get(key) != null : properties.containsKey(key);
}
synchronized public String get(final String key) {
putDefault(key, null);
if (override.containsKey(key))
return override.get(key);
if (!properties.containsKey(key))
return "";
return properties.get(key);
}
synchronized public String get(final String key, final String def) {
putDefault(key, def);
if (override.containsKey(key))
return override.get(key);
final String prop = properties.get(key);
if (prop == null || prop.equals(""))
return def;
return prop;
}
synchronized public Map<String, String> getAllPrefix(final String prefix) {
final Map<String,String> all = new TreeMap<String,String>();
for (final Entry<String,String> e : properties.entrySet())
if (e.getKey().startsWith(prefix)) {
all.put(e.getKey(), e.getValue());
}
for (final Entry<String,String> e : override.entrySet())
if (e.getKey().startsWith(prefix))
if (e.getValue() == null) {
all.remove(e.getKey());
} else {
all.put(e.getKey(), e.getValue());
}
return all;
}
synchronized public TreeMap<String, String> getAllColors() {
final TreeMap<String,String> all = new TreeMap<String,String>();
for (final Entry<String,String> e : defaults.entrySet())
if (e.getKey().startsWith("color.") && e.getValue() != null) {
all.put(e.getKey().substring(6), e.getValue());
}
for (final Entry<String,String> e : properties.entrySet())
if (e.getKey().startsWith("color.")) {
all.put(e.getKey().substring(6), e.getValue());
}
for (final Entry<String,String> e : override.entrySet())
if (e.getKey().startsWith("color."))
if (e.getValue() == null) {
all.remove(e.getKey().substring(6));
} else {
all.put(e.getKey().substring(6), e.getValue());
}
return all;
}
synchronized public Map<String, String> getDefaults() {
return defaults;
}
synchronized public void putDefault(final String key, final String def) {
if(!defaults.containsKey(key) || defaults.get(key) == null) {
defaults.put(key, def);
} else if(def != null && !defaults.get(key).equals(def)) {
System.out.println("Defaults for " + key + " differ: " + def + " != " + defaults.get(key));
}
}
synchronized public boolean getBoolean(final String key) {
putDefault(key, null);
if (override.containsKey(key))
return override.get(key) == null ? false : Boolean.parseBoolean(override.get(key));
return properties.containsKey(key) ? Boolean.parseBoolean(properties.get(key)) : false;
}
synchronized public boolean getBoolean(final String key, final boolean def) {
putDefault(key, Boolean.toString(def));
if (override.containsKey(key))
return override.get(key) == null ? def : Boolean.parseBoolean(override.get(key));
return properties.containsKey(key) ? Boolean.parseBoolean(properties.get(key)) : def;
}
synchronized public boolean put(final String key, String value) {
String oldvalue = properties.get(key);
if(value != null && value.length() == 0) {
value = null;
}
if(!((oldvalue == null && (value == null || value.equals(defaults.get(key))))
|| (value != null && oldvalue != null && oldvalue.equals(value))))
{
if (value == null) {
properties.remove(key);
} else {
properties.put(key, value);
}
try {
save();
} catch(IOException e){
System.out.println(tr("Warning: failed to persist preferences to ''{0}''", getPreferenceFile().getAbsoluteFile()));
}
firePrefrenceChanged(key, oldvalue, value);
return true;
}
return false;
}
synchronized public boolean put(final String key, final boolean value) {
return put(key, Boolean.toString(value));
}
synchronized public boolean putInteger(final String key, final Integer value) {
return put(key, Integer.toString(value));
}
synchronized public boolean putDouble(final String key, final Double value) {
return put(key, Double.toString(value));
}
synchronized public boolean putLong(final String key, final Long value) {
return put(key, Long.toString(value));
}
/**
* Called after every put. In case of a problem, do nothing but output the error
* in log.
*/
public void save() throws IOException {
/* currently unused, but may help to fix configuration issues in future */
putInteger("josm.version", Version.getInstance().getVersion());
updateSystemProperties();
File prefFile = new File(getPreferencesDirFile(), "preferences");
// Backup old preferences if there are old preferences
if(prefFile.exists()) {
copyFile(prefFile, new File(prefFile + "_backup"));
}
final PrintWriter out = new PrintWriter(new OutputStreamWriter(
new FileOutputStream(prefFile + "_tmp"), "utf-8"), false);
for (final Entry<String, String> e : properties.entrySet()) {
String s = defaults.get(e.getKey());
/* don't save default values */
if(s == null || !s.equals(e.getValue())) {
out.println(e.getKey() + "=" + e.getValue());
}
}
out.close();
File tmpFile = new File(prefFile + "_tmp");
copyFile(tmpFile, prefFile);
tmpFile.delete();
}
/**
* Simple file copy function that will overwrite the target file
* Taken from http://www.rgagnon.com/javadetails/java-0064.html (CC-NC-BY-SA)
* @param in
* @param out
* @throws IOException
*/
public static void copyFile(File in, File out) throws IOException {
FileChannel inChannel = new FileInputStream(in).getChannel();
FileChannel outChannel = new FileOutputStream(out).getChannel();
try {
inChannel.transferTo(0, inChannel.size(),
outChannel);
}
catch (IOException e) {
throw e;
}
finally {
if (inChannel != null) {
inChannel.close();
}
if (outChannel != null) {
outChannel.close();
}
}
}
public void load() throws IOException {
properties.clear();
final BufferedReader in = new BufferedReader(new InputStreamReader(
new FileInputStream(getPreferencesDir()+"preferences"), "utf-8"));
int lineNumber = 0;
ArrayList<Integer> errLines = new ArrayList<Integer>();
for (String line = in.readLine(); line != null; line = in.readLine(), lineNumber++) {
final int i = line.indexOf('=');
if (i == -1 || i == 0) {
errLines.add(lineNumber);
continue;
}
properties.put(line.substring(0,i), line.substring(i+1));
}
if (!errLines.isEmpty())
throw new IOException(tr("Malformed config file at lines {0}", errLines));
updateSystemProperties();
}
public void init(boolean reset){
// get the preferences.
File prefDir = getPreferencesDirFile();
if (prefDir.exists()) {
if(!prefDir.isDirectory()) {
System.err.println(tr("Warning: Failed to initialize preferences. Preference directory ''{0}'' is not a directory.", prefDir.getAbsoluteFile()));
JOptionPane.showMessageDialog(
Main.parent,
tr("<html>Failed to initialize preferences.<br>Preference directory ''{0}'' is not a directory.</html>", prefDir.getAbsoluteFile()),
tr("Error"),
JOptionPane.ERROR_MESSAGE
);
return;
}
} else {
if (! prefDir.mkdirs()) {
System.err.println(tr("Warning: Failed to initialize preferences. Failed to create missing preference directory: {0}", prefDir.getAbsoluteFile()));
JOptionPane.showMessageDialog(
Main.parent,
tr("<html>Failed to initialize preferences.<br>Failed to create missing preference directory: {0}</html>",prefDir.getAbsoluteFile()),
tr("Error"),
JOptionPane.ERROR_MESSAGE
);
return;
}
}
File preferenceFile = getPreferenceFile();
try {
if (!preferenceFile.exists()) {
System.out.println(tr("Warning: Missing preference file ''{0}''. Creating a default preference file.", preferenceFile.getAbsoluteFile()));
resetToDefault();
save();
} else if (reset) {
System.out.println(tr("Warning: Replacing existing preference file ''{0}'' with default preference file.", preferenceFile.getAbsoluteFile()));
resetToDefault();
save();
}
} catch(IOException e) {
e.printStackTrace();
JOptionPane.showMessageDialog(
Main.parent,
tr("<html>Failed to initialize preferences.<br>Failed to reset preference file to default: {0}</html>",getPreferenceFile().getAbsoluteFile()),
tr("Error"),
JOptionPane.ERROR_MESSAGE
);
return;
}
try {
load();
} catch (IOException e) {
e.printStackTrace();
File backupFile = new File(prefDir,"preferences.bak");
JOptionPane.showMessageDialog(
Main.parent,
tr("<html>Preferences file had errors.<br> Making backup of old one to <br>{0}<br> and creating a new default preference file.</html>", backupFile.getAbsoluteFile()),
tr("Error"),
JOptionPane.ERROR_MESSAGE
);
preferenceFile.renameTo(backupFile);
try {
resetToDefault();
save();
} catch(IOException e1) {
e1.printStackTrace();
System.err.println(tr("Warning: Failed to initialize preferences.Failed to reset preference file to default: {0}", getPreferenceFile()));
}
}
}
public final void resetToDefault(){
properties.clear();
}
public File getBookmarksFile() {
return new File(getPreferencesDir(),"bookmarks");
}
public Collection<Bookmark> loadBookmarks() throws IOException {
File bookmarkFile = getBookmarksFile();
if (!bookmarkFile.exists()) {
bookmarkFile.createNewFile();
}
BufferedReader in = new BufferedReader(new InputStreamReader(
new FileInputStream(bookmarkFile), "utf-8"));
LinkedList<Bookmark> bookmarks = new LinkedList<Bookmark>();
for (String line = in.readLine(); line != null; line = in.readLine()) {
// FIXME: legacy code using ',' sign, should be \u001e only
Matcher m = Pattern.compile("^(.+)[,\u001e](-?\\d+.\\d+)[,\u001e](-?\\d+.\\d+)[,\u001e](-?\\d+.\\d+)[,\u001e](-?\\d+.\\d+)$").matcher(line);
if (!m.matches() || m.groupCount() != 5) {
System.err.println(tr("Error: Unexpected line ''{0}'' in bookmark file ''{1}''",line, bookmarkFile.toString()));
continue;
}
Bookmark b = new Bookmark();
b.setName(m.group(1));
double[] values= new double[4];
for (int i = 0; i < 4; ++i) {
try {
values[i] = Double.parseDouble(m.group(i+2));
} catch(NumberFormatException e) {
System.err.println(tr("Error: Illegal double value ''{0}'' on line ''{1}'' in bookmark file ''{2}''",m.group(i+2),line, bookmarkFile.toString()));
continue;
}
}
b.setArea(new Bounds(values));
bookmarks.add(b);
}
in.close();
Collections.sort(bookmarks);
return bookmarks;
}
public void saveBookmarks(Collection<Bookmark> bookmarks) throws IOException {
File bookmarkFile = new File(Main.pref.getPreferencesDir()+"bookmarks");
if (!bookmarkFile.exists()) {
bookmarkFile.createNewFile();
}
PrintWriter out = new PrintWriter(new OutputStreamWriter(
new FileOutputStream(bookmarkFile), "utf-8"));
for (Bookmark b : bookmarks) {
out.print(b.getName()+ "\u001e");
Bounds area = b.getArea();
out.print(area.getMin().lat() +"\u001e");
out.print(area.getMin().lon() +"\u001e");
out.print(area.getMax().lat() +"\u001e");
out.print(area.getMax().lon());
out.println();
}
out.close();
}
/**
* Convenience method for accessing colour preferences.
*
* @param colName name of the colour
* @param def default value
* @return a Color object for the configured colour, or the default value if none configured.
*/
synchronized public Color getColor(String colName, Color def) {
return getColor(colName, null, def);
}
public Color getColor(ColorKey key) {
return getColor(key.getColorName(), key.getSpecialName(), key.getDefault());
}
/**
* Convenience method for accessing colour preferences.
*
* @param colName name of the colour
* @param specName name of the special colour settings
* @param def default value
* @return a Color object for the configured colour, or the default value if none configured.
*/
synchronized public Color getColor(String colName, String specName, Color def) {
putDefault("color."+colName, ColorHelper.color2html(def));
String colStr = specName != null ? get("color."+specName) : "";
if(colStr.equals("")) {
colStr = get("color."+colName);
}
return colStr.equals("") ? def : ColorHelper.html2color(colStr);
}
synchronized public Color getDefaultColor(String colName) {
String colStr = defaults.get("color."+colName);
return colStr == null || "".equals(colStr) ? null : ColorHelper.html2color(colStr);
}
synchronized public boolean putColor(String colName, Color val) {
return put("color."+colName, val != null ? ColorHelper.color2html(val) : null);
}
synchronized public int getInteger(String key, int def) {
putDefault(key, Integer.toString(def));
String v = get(key);
if(null == v)
return def;
try {
return Integer.parseInt(v);
} catch(NumberFormatException e) {
// fall out
}
return def;
}
synchronized public long getLong(String key, long def) {
putDefault(key, Long.toString(def));
String v = get(key);
if(null == v)
return def;
try {
return Long.parseLong(v);
} catch(NumberFormatException e) {
// fall out
}
return def;
}
synchronized public double getDouble(String key, double def) {
putDefault(key, Double.toString(def));
String v = get(key);
if(null == v)
return def;
try {
return Double.parseDouble(v);
} catch(NumberFormatException e) {
// fall out
}
return def;
}
synchronized public double getDouble(String key, String def) {
putDefault(key, def);
String v = get(key);
if(v != null && v.length() != 0) {
try { return Double.parseDouble(v); } catch(NumberFormatException e) {}
}
try { return Double.parseDouble(def); } catch(NumberFormatException e) {}
return 0.0;
}
synchronized public String getCollectionAsString(final String key) {
String s = get(key);
if(s != null && s.length() != 0) {
s = s.replaceAll("\u001e",",");
}
return s;
}
public boolean isCollection(String key, boolean def) {
String s = get(key);
if (s != null && s.length() != 0)
return s.indexOf("\u001e") >= 0 || s.indexOf("§§§") >= 0;
else
return def;
}
synchronized public Collection<String> getCollection(String key, Collection<String> def) {
String s = get(key);
if(def != null)
{
String d = null;
for(String a : def)
{
if(d != null) {
d += "\u001e" + a;
} else {
d = a;
}
}
putDefault(key, d);
}
if(s != null && s.length() != 0)
{
if(s.indexOf("\u001e") < 0) /* FIXME: legacy code, remove later */
{
String r =s;
if(r.indexOf("§§§") > 0) {
r = r.replaceAll("§§§","\u001e");
} else {
r = r.replace(';','\u001e');
}
if(!r.equals(s)) /* save the converted string */
{
put(key,r);
s = r;
}
}
return Arrays.asList(s.split("\u001e"));
}
return def;
}
synchronized public void removeFromCollection(String key, String value) {
List<String> a = new ArrayList<String>(getCollection(key, null));
a.remove(value);
putCollection(key, a);
}
synchronized public boolean putCollection(String key, Collection<String> val) {
String s = null;
if(val != null)
{
for(String a : val)
{
if(s != null) {
s += "\u001e" + a;
} else {
s = a;
}
}
}
return put(key, s);
}
/**
* Updates system properties with the current values in the preferences.
*
*/
public void updateSystemProperties() {
Properties sysProp = System.getProperties();
sysProp.put("http.agent", Version.getInstance().getAgentString());
System.setProperties(sysProp);
}
/**
* The default plugin site
*/
private final static String[] DEFAULT_PLUGIN_SITE = {"http://josm.openstreetmap.de/plugin"};
/**
* Replies the collection of plugin site URLs from where plugin lists can be downloaded
*
* @return
*/
public Collection<String> getPluginSites() {
return getCollection("pluginmanager.sites", Arrays.asList(DEFAULT_PLUGIN_SITE));
}
/**
* Sets the collection of plugin site URLs.
*
* @param sites the site URLs
*/
public void setPluginSites(Collection<String> sites) {
putCollection("pluginmanager.sites", sites);
}
}