001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Color;
007import java.awt.Toolkit;
008import java.io.BufferedReader;
009import java.io.File;
010import java.io.FileInputStream;
011import java.io.FileOutputStream;
012import java.io.IOException;
013import java.io.InputStreamReader;
014import java.io.OutputStreamWriter;
015import java.io.PrintWriter;
016import java.io.Reader;
017import java.lang.annotation.Retention;
018import java.lang.annotation.RetentionPolicy;
019import java.lang.reflect.Field;
020import java.util.ArrayList;
021import java.util.Arrays;
022import java.util.Collection;
023import java.util.Collections;
024import java.util.Iterator;
025import java.util.LinkedHashMap;
026import java.util.LinkedList;
027import java.util.List;
028import java.util.Map;
029import java.util.Map.Entry;
030import java.util.ResourceBundle;
031import java.util.SortedMap;
032import java.util.TreeMap;
033import java.util.concurrent.CopyOnWriteArrayList;
034import java.util.regex.Matcher;
035import java.util.regex.Pattern;
036
037import javax.swing.JOptionPane;
038import javax.swing.UIManager;
039import javax.xml.XMLConstants;
040import javax.xml.stream.XMLInputFactory;
041import javax.xml.stream.XMLStreamConstants;
042import javax.xml.stream.XMLStreamException;
043import javax.xml.stream.XMLStreamReader;
044import javax.xml.transform.stream.StreamSource;
045import javax.xml.validation.Schema;
046import javax.xml.validation.SchemaFactory;
047import javax.xml.validation.Validator;
048
049import org.openstreetmap.josm.Main;
050import org.openstreetmap.josm.data.preferences.ColorProperty;
051import org.openstreetmap.josm.io.MirroredInputStream;
052import org.openstreetmap.josm.io.XmlWriter;
053import org.openstreetmap.josm.tools.ColorHelper;
054import org.openstreetmap.josm.tools.Utils;
055
056/**
057 * This class holds all preferences for JOSM.
058 *
059 * Other classes can register their beloved properties here. All properties will be
060 * saved upon set-access.
061 *
062 * Each property is a key=setting pair, where key is a String and setting can be one of
063 * 4 types:
064 *     string, list, list of lists and list of maps.
065 * In addition, each key has a unique default value that is set when the value is first
066 * accessed using one of the get...() methods. You can use the same preference
067 * key in different parts of the code, but the default value must be the same
068 * everywhere. A default value of null means, the setting has been requested, but
069 * no default value was set. This is used in advanced preferences to present a list
070 * off all possible settings.
071 *
072 * At the moment, you cannot put the empty string for string properties.
073 * put(key, "") means, the property is removed.
074 *
075 * @author imi
076 */
077public class Preferences {
078    /**
079     * Internal storage for the preference directory.
080     * Do not access this variable directly!
081     * @see #getPreferencesDirFile()
082     */
083    private File preferencesDirFile = null;
084    /**
085     * Internal storage for the cache directory.
086     */
087    private File cacheDirFile = null;
088
089    /**
090     * Map the property name to strings. Does not contain null or "" values.
091     */
092    protected final SortedMap<String, String> properties = new TreeMap<String, String>();
093    /** Map of defaults, can contain null values */
094    protected final SortedMap<String, String> defaults = new TreeMap<String, String>();
095    protected final SortedMap<String, String> colornames = new TreeMap<String, String>();
096
097    /** Mapping for list settings. Must not contain null values */
098    protected final SortedMap<String, List<String>> collectionProperties = new TreeMap<String, List<String>>();
099    /** Defaults, can contain null values */
100    protected final SortedMap<String, List<String>> collectionDefaults = new TreeMap<String, List<String>>();
101
102    protected final SortedMap<String, List<List<String>>> arrayProperties = new TreeMap<String, List<List<String>>>();
103    protected final SortedMap<String, List<List<String>>> arrayDefaults = new TreeMap<String, List<List<String>>>();
104
105    protected final SortedMap<String, List<Map<String,String>>> listOfStructsProperties = new TreeMap<String, List<Map<String,String>>>();
106    protected final SortedMap<String, List<Map<String,String>>> listOfStructsDefaults = new TreeMap<String, List<Map<String,String>>>();
107
108    /**
109     * Interface for a preference value
110     *
111     * @param <T> the data type for the value
112     */
113    public interface Setting<T> {
114        /**
115         * Returns the value of this setting.
116         *
117         * @return the value of this setting
118         */
119        T getValue();
120
121        /**
122         * Enable usage of the visitor pattern.
123         *
124         * @param visitor the visitor
125         */
126        void visit(SettingVisitor visitor);
127
128        /**
129         * Returns a setting whose value is null.
130         *
131         * Cannot be static, because there is no static inheritance.
132         * @return a Setting object that isn't null itself, but returns null
133         * for {@link #getValue()}
134         */
135        Setting<T> getNullInstance();
136    }
137
138    /**
139     * Base abstract class of all settings, holding the setting value.
140     *
141     * @param <T> The setting type
142     */
143    abstract public static class AbstractSetting<T> implements Setting<T> {
144        private final T value;
145        /**
146         * Constructs a new {@code AbstractSetting} with the given value
147         * @param value The setting value
148         */
149        public AbstractSetting(T value) {
150            this.value = value;
151        }
152        @Override public T getValue() {
153            return value;
154        }
155        @Override public String toString() {
156            return value != null ? value.toString() : "null";
157        }
158    }
159
160    /**
161     * Setting containing a {@link String} value.
162     */
163    public static class StringSetting extends AbstractSetting<String> {
164        /**
165         * Constructs a new {@code StringSetting} with the given value
166         * @param value The setting value
167         */
168        public StringSetting(String value) {
169            super(value);
170        }
171        @Override public void visit(SettingVisitor visitor) {
172            visitor.visit(this);
173        }
174        @Override public StringSetting getNullInstance() {
175            return new StringSetting(null);
176        }
177    }
178
179    /**
180     * Setting containing a {@link List} of {@link String} values.
181     */
182    public static class ListSetting extends AbstractSetting<List<String>> {
183        /**
184         * Constructs a new {@code ListSetting} with the given value
185         * @param value The setting value
186         */
187        public ListSetting(List<String> value) {
188            super(value);
189        }
190        @Override public void visit(SettingVisitor visitor) {
191            visitor.visit(this);
192        }
193        @Override public ListSetting getNullInstance() {
194            return new ListSetting(null);
195        }
196    }
197
198    /**
199     * Setting containing a {@link List} of {@code List}s of {@link String} values.
200     */
201    public static class ListListSetting extends AbstractSetting<List<List<String>>> {
202        /**
203         * Constructs a new {@code ListListSetting} with the given value
204         * @param value The setting value
205         */
206        public ListListSetting(List<List<String>> value) {
207            super(value);
208        }
209        @Override public void visit(SettingVisitor visitor) {
210            visitor.visit(this);
211        }
212        @Override public ListListSetting getNullInstance() {
213            return new ListListSetting(null);
214        }
215    }
216
217    /**
218     * Setting containing a {@link List} of {@link Map}s of {@link String} values.
219     */
220    public static class MapListSetting extends AbstractSetting<List<Map<String, String>>> {
221        /**
222         * Constructs a new {@code MapListSetting} with the given value
223         * @param value The setting value
224         */
225        public MapListSetting(List<Map<String, String>> value) {
226            super(value);
227        }
228        @Override public void visit(SettingVisitor visitor) {
229            visitor.visit(this);
230        }
231        @Override public MapListSetting getNullInstance() {
232            return new MapListSetting(null);
233        }
234    }
235
236    public interface SettingVisitor {
237        void visit(StringSetting setting);
238        void visit(ListSetting value);
239        void visit(ListListSetting value);
240        void visit(MapListSetting value);
241    }
242
243    public interface PreferenceChangeEvent<T> {
244        String getKey();
245        Setting<T> getOldValue();
246        Setting<T> getNewValue();
247    }
248
249    public interface PreferenceChangedListener {
250        void preferenceChanged(PreferenceChangeEvent e);
251    }
252
253    private static class DefaultPreferenceChangeEvent<T> implements PreferenceChangeEvent<T> {
254        private final String key;
255        private final Setting<T> oldValue;
256        private final Setting<T> newValue;
257
258        public DefaultPreferenceChangeEvent(String key, Setting<T> oldValue, Setting<T> newValue) {
259            this.key = key;
260            this.oldValue = oldValue;
261            this.newValue = newValue;
262        }
263
264        @Override
265        public String getKey() {
266            return key;
267        }
268        @Override
269        public Setting<T> getOldValue() {
270            return oldValue;
271        }
272        @Override
273        public Setting<T> getNewValue() {
274            return newValue;
275        }
276    }
277
278    public interface ColorKey {
279        String getColorName();
280        String getSpecialName();
281        Color getDefaultValue();
282    }
283
284    private final CopyOnWriteArrayList<PreferenceChangedListener> listeners = new CopyOnWriteArrayList<PreferenceChangedListener>();
285
286    public void addPreferenceChangeListener(PreferenceChangedListener listener) {
287        if (listener != null) {
288            listeners.addIfAbsent(listener);
289        }
290    }
291
292    public void removePreferenceChangeListener(PreferenceChangedListener listener) {
293        listeners.remove(listener);
294    }
295
296    protected <T> void firePreferenceChanged(String key, Setting<T> oldValue, Setting<T> newValue) {
297        PreferenceChangeEvent<T> evt = new DefaultPreferenceChangeEvent<T>(key, oldValue, newValue);
298        for (PreferenceChangedListener l : listeners) {
299            l.preferenceChanged(evt);
300        }
301    }
302
303    /**
304     * Returns the location of the user defined preferences directory
305     * @return The location of the user defined preferences directory
306     */
307    public String getPreferencesDir() {
308        final String path = getPreferencesDirFile().getPath();
309        if (path.endsWith(File.separator))
310            return path;
311        return path + File.separator;
312    }
313
314    /**
315     * Returns the user defined preferences directory
316     * @return The user defined preferences directory
317     */
318    public File getPreferencesDirFile() {
319        if (preferencesDirFile != null)
320            return preferencesDirFile;
321        String path;
322        path = System.getProperty("josm.home");
323        if (path != null) {
324            preferencesDirFile = new File(path).getAbsoluteFile();
325        } else {
326            path = System.getenv("APPDATA");
327            if (path != null) {
328                preferencesDirFile = new File(path, "JOSM");
329            } else {
330                preferencesDirFile = new File(System.getProperty("user.home"), ".josm");
331            }
332        }
333        return preferencesDirFile;
334    }
335
336    /**
337     * Returns the user preferences file
338     * @return The user preferences file
339     */
340    public File getPreferenceFile() {
341        return new File(getPreferencesDirFile(), "preferences.xml");
342    }
343
344    /**
345     * Returns the user plugin directory
346     * @return The user plugin directory
347     */
348    public File getPluginsDirectory() {
349        return new File(getPreferencesDirFile(), "plugins");
350    }
351
352    /**
353     * Get the directory where cached content of any kind should be stored.
354     *
355     * If the directory doesn't exist on the file system, it will be created
356     * by this method.
357     *
358     * @return the cache directory
359     */
360    public File getCacheDirectory() {
361        if (cacheDirFile != null)
362            return cacheDirFile;
363        String path = System.getProperty("josm.cache");
364        if (path != null) {
365            cacheDirFile = new File(path).getAbsoluteFile();
366        } else {
367            path = get("cache.folder", null);
368            if (path != null) {
369                cacheDirFile = new File(path);
370            } else {
371                cacheDirFile = new File(getPreferencesDirFile(), "cache");
372            }
373        }
374        if (!cacheDirFile.exists() && !cacheDirFile.mkdirs()) {
375            Main.warn(tr("Failed to create missing cache directory: {0}", cacheDirFile.getAbsoluteFile()));
376            JOptionPane.showMessageDialog(
377                    Main.parent,
378                    tr("<html>Failed to create missing cache directory: {0}</html>", cacheDirFile.getAbsoluteFile()),
379                    tr("Error"),
380                    JOptionPane.ERROR_MESSAGE
381            );
382        }
383        return cacheDirFile;
384    }
385
386    /**
387     * @return A list of all existing directories where resources could be stored.
388     */
389    public Collection<String> getAllPossiblePreferenceDirs() {
390        LinkedList<String> locations = new LinkedList<String>();
391        locations.add(getPreferencesDir());
392        String s;
393        if ((s = System.getenv("JOSM_RESOURCES")) != null) {
394            if (!s.endsWith(File.separator)) {
395                s = s + File.separator;
396            }
397            locations.add(s);
398        }
399        if ((s = System.getProperty("josm.resources")) != null) {
400            if (!s.endsWith(File.separator)) {
401                s = s + File.separator;
402            }
403            locations.add(s);
404        }
405        String appdata = System.getenv("APPDATA");
406        if (System.getenv("ALLUSERSPROFILE") != null && appdata != null
407                && appdata.lastIndexOf(File.separator) != -1) {
408            appdata = appdata.substring(appdata.lastIndexOf(File.separator));
409            locations.add(new File(new File(System.getenv("ALLUSERSPROFILE"),
410                    appdata), "JOSM").getPath());
411        }
412        locations.add("/usr/local/share/josm/");
413        locations.add("/usr/local/lib/josm/");
414        locations.add("/usr/share/josm/");
415        locations.add("/usr/lib/josm/");
416        return locations;
417    }
418
419    /**
420     * Get settings value for a certain key.
421     * @param key the identifier for the setting
422     * @return "" if there is nothing set for the preference key,
423     *  the corresponding value otherwise. The result is not null.
424     */
425    synchronized public String get(final String key) {
426        putDefault(key, null);
427        if (!properties.containsKey(key))
428            return "";
429        return properties.get(key);
430    }
431
432    /**
433     * Get settings value for a certain key and provide default a value.
434     * @param key the identifier for the setting
435     * @param def the default value. For each call of get() with a given key, the
436     *  default value must be the same.
437     * @return the corresponding value if the property has been set before,
438     *  def otherwise
439     */
440    synchronized public String get(final String key, final String def) {
441        putDefault(key, def);
442        final String prop = properties.get(key);
443        if (prop == null || prop.isEmpty())
444            return def;
445        return prop;
446    }
447
448    synchronized public Map<String, String> getAllPrefix(final String prefix) {
449        final Map<String,String> all = new TreeMap<String,String>();
450        for (final Entry<String,String> e : properties.entrySet()) {
451            if (e.getKey().startsWith(prefix)) {
452                all.put(e.getKey(), e.getValue());
453            }
454        }
455        return all;
456    }
457
458    synchronized public List<String> getAllPrefixCollectionKeys(final String prefix) {
459        final List<String> all = new LinkedList<String>();
460        for (final String e : collectionProperties.keySet()) {
461            if (e.startsWith(prefix)) {
462                all.add(e);
463            }
464        }
465        return all;
466    }
467
468    synchronized public Map<String, String> getAllColors() {
469        final Map<String,String> all = new TreeMap<String,String>();
470        for (final Entry<String,String> e : defaults.entrySet()) {
471            if (e.getKey().startsWith("color.") && e.getValue() != null) {
472                all.put(e.getKey().substring(6), e.getValue());
473            }
474        }
475        for (final Entry<String,String> e : properties.entrySet()) {
476            if (e.getKey().startsWith("color.")) {
477                all.put(e.getKey().substring(6), e.getValue());
478            }
479        }
480        return all;
481    }
482
483    synchronized public Map<String, String> getDefaults() {
484        return defaults;
485    }
486
487    synchronized public void putDefault(final String key, final String def) {
488        if(!defaults.containsKey(key) || defaults.get(key) == null) {
489            defaults.put(key, def);
490        } else if(def != null && !defaults.get(key).equals(def)) {
491            Main.info("Defaults for " + key + " differ: " + def + " != " + defaults.get(key));
492        }
493    }
494
495    synchronized public boolean getBoolean(final String key) {
496        putDefault(key, null);
497        return properties.containsKey(key) ? Boolean.parseBoolean(properties.get(key)) : false;
498    }
499
500    synchronized public boolean getBoolean(final String key, final boolean def) {
501        putDefault(key, Boolean.toString(def));
502        return properties.containsKey(key) ? Boolean.parseBoolean(properties.get(key)) : def;
503    }
504
505    synchronized public boolean getBoolean(final String key, final String specName, final boolean def) {
506        putDefault(key, Boolean.toString(def));
507        String skey = key+"."+specName;
508        if(properties.containsKey(skey))
509            return Boolean.parseBoolean(properties.get(skey));
510        return properties.containsKey(key) ? Boolean.parseBoolean(properties.get(key)) : def;
511    }
512
513    /**
514     * Set a value for a certain setting. The changed setting is saved
515     * to the preference file immediately. Due to caching mechanisms on modern
516     * operating systems and hardware, this shouldn't be a performance problem.
517     * @param key the unique identifier for the setting
518     * @param value the value of the setting. Can be null or "" which both removes
519     *  the key-value entry.
520     * @return if true, something has changed (i.e. value is different than before)
521     */
522    public boolean put(final String key, String value) {
523        boolean changed = false;
524        String oldValue = null;
525
526        synchronized (this) {
527            oldValue = properties.get(key);
528            if(value != null && value.length() == 0) {
529                value = null;
530            }
531            // value is the same as before - no need to save anything
532            boolean equalValue = oldValue != null && oldValue.equals(value);
533            // The setting was previously unset and we are supposed to put a
534            // value that equals the default value. This is not necessary because
535            // the default value is the same throughout josm. In addition we like
536            // to have the possibility to change the default value from version
537            // to version, which would not work if we wrote it to the preference file.
538            boolean unsetIsDefault = oldValue == null && (value == null || value.equals(defaults.get(key)));
539
540            if (!(equalValue || unsetIsDefault)) {
541                if (value == null) {
542                    properties.remove(key);
543                } else {
544                    properties.put(key, value);
545                }
546                try {
547                    save();
548                } catch (IOException e) {
549                    Main.warn(tr("Failed to persist preferences to ''{0}''", getPreferenceFile().getAbsoluteFile()));
550                }
551                changed = true;
552            }
553        }
554        if (changed) {
555            // Call outside of synchronized section in case some listener wait for other thread that wait for preference lock
556            firePreferenceChanged(key, new StringSetting(oldValue), new StringSetting(value));
557        }
558        return changed;
559    }
560
561    public boolean put(final String key, final boolean value) {
562        return put(key, Boolean.toString(value));
563    }
564
565    public boolean putInteger(final String key, final Integer value) {
566        return put(key, Integer.toString(value));
567    }
568
569    public boolean putDouble(final String key, final Double value) {
570        return put(key, Double.toString(value));
571    }
572
573    public boolean putLong(final String key, final Long value) {
574        return put(key, Long.toString(value));
575    }
576
577    /**
578     * Called after every put. In case of a problem, do nothing but output the error
579     * in log.
580     */
581    public void save() throws IOException {
582        /* currently unused, but may help to fix configuration issues in future */
583        putInteger("josm.version", Version.getInstance().getVersion());
584
585        updateSystemProperties();
586        if(Main.applet)
587            return;
588
589        File prefFile = getPreferenceFile();
590        File backupFile = new File(prefFile + "_backup");
591
592        // Backup old preferences if there are old preferences
593        if (prefFile.exists()) {
594            Utils.copyFile(prefFile, backupFile);
595        }
596
597        final PrintWriter out = new PrintWriter(new OutputStreamWriter(
598                new FileOutputStream(prefFile + "_tmp"), "utf-8"), false);
599        out.print(toXML(false));
600        Utils.close(out);
601
602        File tmpFile = new File(prefFile + "_tmp");
603        Utils.copyFile(tmpFile, prefFile);
604        tmpFile.delete();
605
606        setCorrectPermissions(prefFile);
607        setCorrectPermissions(backupFile);
608    }
609
610
611    private void setCorrectPermissions(File file) {
612        file.setReadable(false, false);
613        file.setWritable(false, false);
614        file.setExecutable(false, false);
615        file.setReadable(true, true);
616        file.setWritable(true, true);
617    }
618
619    public void load() throws Exception {
620        properties.clear();
621        if (!Main.applet) {
622            File pref = getPreferenceFile();
623            BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(pref), "utf-8"));
624            try {
625                validateXML(in);
626                Utils.close(in);
627                in = new BufferedReader(new InputStreamReader(new FileInputStream(pref), "utf-8"));
628                fromXML(in);
629            } finally {
630                Utils.close(in);
631            }
632        }
633        updateSystemProperties();
634        removeObsolete();
635    }
636
637    public void init(boolean reset){
638        if(Main.applet)
639            return;
640        // get the preferences.
641        File prefDir = getPreferencesDirFile();
642        if (prefDir.exists()) {
643            if(!prefDir.isDirectory()) {
644                Main.warn(tr("Failed to initialize preferences. Preference directory ''{0}'' is not a directory.", prefDir.getAbsoluteFile()));
645                JOptionPane.showMessageDialog(
646                        Main.parent,
647                        tr("<html>Failed to initialize preferences.<br>Preference directory ''{0}'' is not a directory.</html>", prefDir.getAbsoluteFile()),
648                        tr("Error"),
649                        JOptionPane.ERROR_MESSAGE
650                );
651                return;
652            }
653        } else {
654            if (! prefDir.mkdirs()) {
655                Main.warn(tr("Failed to initialize preferences. Failed to create missing preference directory: {0}", prefDir.getAbsoluteFile()));
656                JOptionPane.showMessageDialog(
657                        Main.parent,
658                        tr("<html>Failed to initialize preferences.<br>Failed to create missing preference directory: {0}</html>",prefDir.getAbsoluteFile()),
659                        tr("Error"),
660                        JOptionPane.ERROR_MESSAGE
661                );
662                return;
663            }
664        }
665
666        File preferenceFile = getPreferenceFile();
667        try {
668            if (!preferenceFile.exists()) {
669                Main.info(tr("Missing preference file ''{0}''. Creating a default preference file.", preferenceFile.getAbsoluteFile()));
670                resetToDefault();
671                save();
672            } else if (reset) {
673                Main.warn(tr("Replacing existing preference file ''{0}'' with default preference file.", preferenceFile.getAbsoluteFile()));
674                resetToDefault();
675                save();
676            }
677        } catch(IOException e) {
678            e.printStackTrace();
679            JOptionPane.showMessageDialog(
680                    Main.parent,
681                    tr("<html>Failed to initialize preferences.<br>Failed to reset preference file to default: {0}</html>",getPreferenceFile().getAbsoluteFile()),
682                    tr("Error"),
683                    JOptionPane.ERROR_MESSAGE
684            );
685            return;
686        }
687        try {
688            load();
689        } catch (Exception e) {
690            e.printStackTrace();
691            File backupFile = new File(prefDir,"preferences.xml.bak");
692            JOptionPane.showMessageDialog(
693                    Main.parent,
694                    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()),
695                    tr("Error"),
696                    JOptionPane.ERROR_MESSAGE
697            );
698            Main.platform.rename(preferenceFile, backupFile);
699            try {
700                resetToDefault();
701                save();
702            } catch(IOException e1) {
703                e1.printStackTrace();
704                Main.warn(tr("Failed to initialize preferences. Failed to reset preference file to default: {0}", getPreferenceFile()));
705            }
706        }
707    }
708
709    public final void resetToDefault(){
710        properties.clear();
711    }
712
713    /**
714     * Convenience method for accessing colour preferences.
715     *
716     * @param colName name of the colour
717     * @param def default value
718     * @return a Color object for the configured colour, or the default value if none configured.
719     */
720    synchronized public Color getColor(String colName, Color def) {
721        return getColor(colName, null, def);
722    }
723
724    synchronized public Color getUIColor(String colName) {
725        return UIManager.getColor(colName);
726    }
727
728    /* only for preferences */
729    synchronized public String getColorName(String o) {
730        try {
731            Matcher m = Pattern.compile("mappaint\\.(.+?)\\.(.+)").matcher(o);
732            if (m.matches()) {
733                return tr("Paint style {0}: {1}", tr(m.group(1)), tr(m.group(2)));
734            }
735        } catch (Exception e) {
736            Main.warn(e);
737        }
738        try {
739            Matcher m = Pattern.compile("layer (.+)").matcher(o);
740            if (m.matches()) {
741                return tr("Layer: {0}", tr(m.group(1)));
742            }
743        } catch (Exception e) {
744            Main.warn(e);
745        }
746        return tr(colornames.containsKey(o) ? colornames.get(o) : o);
747    }
748
749    public Color getColor(ColorKey key) {
750        return getColor(key.getColorName(), key.getSpecialName(), key.getDefaultValue());
751    }
752
753    /**
754     * Convenience method for accessing colour preferences.
755     *
756     * @param colName name of the colour
757     * @param specName name of the special colour settings
758     * @param def default value
759     * @return a Color object for the configured colour, or the default value if none configured.
760     */
761    synchronized public Color getColor(String colName, String specName, Color def) {
762        String colKey = ColorProperty.getColorKey(colName);
763        if(!colKey.equals(colName)) {
764            colornames.put(colKey, colName);
765        }
766        putDefault("color."+colKey, ColorHelper.color2html(def));
767        String colStr = specName != null ? get("color."+specName) : "";
768        if(colStr.isEmpty()) {
769            colStr = get("color."+colKey);
770        }
771        return colStr.isEmpty() ? def : ColorHelper.html2color(colStr);
772    }
773
774    synchronized public Color getDefaultColor(String colKey) {
775        String colStr = defaults.get("color."+colKey);
776        return colStr == null || colStr.isEmpty() ? null : ColorHelper.html2color(colStr);
777    }
778
779    synchronized public boolean putColor(String colKey, Color val) {
780        return put("color."+colKey, val != null ? ColorHelper.color2html(val) : null);
781    }
782
783    synchronized public int getInteger(String key, int def) {
784        putDefault(key, Integer.toString(def));
785        String v = get(key);
786        if(v.isEmpty())
787            return def;
788
789        try {
790            return Integer.parseInt(v);
791        } catch(NumberFormatException e) {
792            // fall out
793        }
794        return def;
795    }
796
797    synchronized public int getInteger(String key, String specName, int def) {
798        putDefault(key, Integer.toString(def));
799        String v = get(key+"."+specName);
800        if(v.isEmpty())
801            v = get(key);
802        if(v.isEmpty())
803            return def;
804
805        try {
806            return Integer.parseInt(v);
807        } catch(NumberFormatException e) {
808            // fall out
809        }
810        return def;
811    }
812
813    synchronized public long getLong(String key, long def) {
814        putDefault(key, Long.toString(def));
815        String v = get(key);
816        if(null == v)
817            return def;
818
819        try {
820            return Long.parseLong(v);
821        } catch(NumberFormatException e) {
822            // fall out
823        }
824        return def;
825    }
826
827    synchronized public double getDouble(String key, double def) {
828        putDefault(key, Double.toString(def));
829        String v = get(key);
830        if(null == v)
831            return def;
832
833        try {
834            return Double.parseDouble(v);
835        } catch(NumberFormatException e) {
836            // fall out
837        }
838        return def;
839    }
840
841    /**
842     * Get a list of values for a certain key
843     * @param key the identifier for the setting
844     * @param def the default value.
845     * @return the corresponding value if the property has been set before,
846     *  def otherwise
847     */
848    public Collection<String> getCollection(String key, Collection<String> def) {
849        putCollectionDefault(key, def == null ? null : new ArrayList<String>(def));
850        Collection<String> prop = collectionProperties.get(key);
851        if (prop != null)
852            return prop;
853        else
854            return def;
855    }
856
857    /**
858     * Get a list of values for a certain key
859     * @param key the identifier for the setting
860     * @return the corresponding value if the property has been set before,
861     *  an empty Collection otherwise.
862     */
863    public Collection<String> getCollection(String key) {
864        putCollectionDefault(key, null);
865        Collection<String> prop = collectionProperties.get(key);
866        if (prop != null)
867            return prop;
868        else
869            return Collections.emptyList();
870    }
871
872    synchronized public void removeFromCollection(String key, String value) {
873        List<String> a = new ArrayList<String>(getCollection(key, Collections.<String>emptyList()));
874        a.remove(value);
875        putCollection(key, a);
876    }
877
878    public boolean putCollection(String key, Collection<String> value) {
879        List<String> oldValue = null;
880        List<String> valueCopy = null;
881
882        synchronized (this) {
883            if (value == null) {
884                oldValue = collectionProperties.remove(key);
885                boolean changed = oldValue != null;
886                changed |= properties.remove(key) != null;
887                if (!changed) return false;
888            } else {
889                oldValue = collectionProperties.get(key);
890                if (equalCollection(value, oldValue)) return false;
891                Collection<String> defValue = collectionDefaults.get(key);
892                if (oldValue == null && equalCollection(value, defValue)) return false;
893
894                valueCopy = new ArrayList<String>(value);
895                if (valueCopy.contains(null)) throw new RuntimeException("Error: Null as list element in preference setting (key '"+key+"')");
896                collectionProperties.put(key, Collections.unmodifiableList(valueCopy));
897            }
898            try {
899                save();
900            } catch (IOException e){
901                Main.warn(tr("Failed to persist preferences to ''{0}''", getPreferenceFile().getAbsoluteFile()));
902            }
903        }
904        // Call outside of synchronized section in case some listener wait for other thread that wait for preference lock
905        firePreferenceChanged(key, new ListSetting(oldValue), new ListSetting(valueCopy));
906        return true;
907    }
908
909    public static boolean equalCollection(Collection<String> a, Collection<String> b) {
910        if (a == null) return b == null;
911        if (b == null) return false;
912        if (a.size() != b.size()) return false;
913        Iterator<String> itA = a.iterator();
914        Iterator<String> itB = b.iterator();
915        while (itA.hasNext()) {
916            String aStr = itA.next();
917            String bStr = itB.next();
918            if (!Utils.equal(aStr,bStr)) return false;
919        }
920        return true;
921    }
922
923    /**
924     * Saves at most {@code maxsize} items of collection {@code val}.
925     */
926    public boolean putCollectionBounded(String key, int maxsize, Collection<String> val) {
927        Collection<String> newCollection = new ArrayList<String>(Math.min(maxsize, val.size()));
928        for (String i : val) {
929            if (newCollection.size() >= maxsize) {
930                break;
931            }
932            newCollection.add(i);
933        }
934        return putCollection(key, newCollection);
935    }
936
937    synchronized private void putCollectionDefault(String key, List<String> val) {
938        collectionDefaults.put(key, val);
939    }
940
941    /**
942     * Used to read a 2-dimensional array of strings from the preference file.
943     * If not a single entry could be found, def is returned.
944     */
945    synchronized public Collection<Collection<String>> getArray(String key, Collection<Collection<String>> def) {
946        if (def != null) {
947            List<List<String>> defCopy = new ArrayList<List<String>>(def.size());
948            for (Collection<String> lst : def) {
949                defCopy.add(Collections.unmodifiableList(new ArrayList<String>(lst)));
950            }
951            putArrayDefault(key, Collections.unmodifiableList(defCopy));
952        } else {
953            putArrayDefault(key, null);
954        }
955        List<List<String>> prop = arrayProperties.get(key);
956        if (prop != null) {
957            @SuppressWarnings({ "unchecked", "rawtypes" })
958            Collection<Collection<String>> prop_cast = (Collection) prop;
959            return prop_cast;
960        } else
961            return def;
962    }
963
964    public Collection<Collection<String>> getArray(String key) {
965        putArrayDefault(key, null);
966        List<List<String>> prop = arrayProperties.get(key);
967        if (prop != null) {
968            @SuppressWarnings({ "unchecked", "rawtypes" })
969            Collection<Collection<String>> prop_cast = (Collection) prop;
970            return prop_cast;
971        } else
972            return Collections.emptyList();
973    }
974
975    public boolean putArray(String key, Collection<Collection<String>> value) {
976        List<List<String>> oldValue = null;
977        List<List<String>> valueCopy = null;
978
979        synchronized (this) {
980            oldValue = arrayProperties.get(key);
981            if (value == null) {
982                if (arrayProperties.remove(key) != null) return false;
983            } else {
984                if (equalArray(value, oldValue)) return false;
985
986                List<List<String>> defValue = arrayDefaults.get(key);
987                if (oldValue == null && equalArray(value, defValue)) return false;
988
989                valueCopy = new ArrayList<List<String>>(value.size());
990                if (valueCopy.contains(null)) throw new RuntimeException("Error: Null as list element in preference setting (key '"+key+"')");
991                for (Collection<String> lst : value) {
992                    List<String> lstCopy = new ArrayList<String>(lst);
993                    if (lstCopy.contains(null)) throw new RuntimeException("Error: Null as inner list element in preference setting (key '"+key+"')");
994                    valueCopy.add(Collections.unmodifiableList(lstCopy));
995                }
996                arrayProperties.put(key, Collections.unmodifiableList(valueCopy));
997            }
998            try {
999                save();
1000            } catch (IOException e){
1001                Main.warn(tr("Failed to persist preferences to ''{0}''", getPreferenceFile().getAbsoluteFile()));
1002            }
1003        }
1004        // Call outside of synchronized section in case some listener wait for other thread that wait for preference lock
1005        firePreferenceChanged(key, new ListListSetting(oldValue), new ListListSetting(valueCopy));
1006        return true;
1007    }
1008
1009    public static boolean equalArray(Collection<Collection<String>> a, Collection<List<String>> b) {
1010        if (a == null) return b == null;
1011        if (b == null) return false;
1012        if (a.size() != b.size()) return false;
1013        Iterator<Collection<String>> itA = a.iterator();
1014        Iterator<List<String>> itB = b.iterator();
1015        while (itA.hasNext()) {
1016            if (!equalCollection(itA.next(), itB.next())) return false;
1017        }
1018        return true;
1019    }
1020
1021    synchronized private void putArrayDefault(String key, List<List<String>> val) {
1022        arrayDefaults.put(key, val);
1023    }
1024
1025    public Collection<Map<String, String>> getListOfStructs(String key, Collection<Map<String, String>> def) {
1026        if (def != null) {
1027            List<Map<String, String>> defCopy = new ArrayList<Map<String, String>>(def.size());
1028            for (Map<String, String> map : def) {
1029                defCopy.add(Collections.unmodifiableMap(new LinkedHashMap<String,String>(map)));
1030            }
1031            putListOfStructsDefault(key, Collections.unmodifiableList(defCopy));
1032        } else {
1033            putListOfStructsDefault(key, null);
1034        }
1035        Collection<Map<String, String>> prop = listOfStructsProperties.get(key);
1036        if (prop != null)
1037            return prop;
1038        else
1039            return def;
1040    }
1041
1042    public boolean putListOfStructs(String key, Collection<Map<String, String>> value) {
1043
1044        List<Map<String, String>> oldValue;
1045        List<Map<String, String>> valueCopy = null;
1046
1047        synchronized (this) {
1048            oldValue = listOfStructsProperties.get(key);
1049            if (value == null) {
1050                if (listOfStructsProperties.remove(key) != null) return false;
1051            } else {
1052                if (equalListOfStructs(oldValue, value)) return false;
1053
1054                List<Map<String, String>> defValue = listOfStructsDefaults.get(key);
1055                if (oldValue == null && equalListOfStructs(value, defValue)) return false;
1056
1057                valueCopy = new ArrayList<Map<String, String>>(value.size());
1058                if (valueCopy.contains(null)) throw new RuntimeException("Error: Null as list element in preference setting (key '"+key+"')");
1059                for (Map<String, String> map : value) {
1060                    Map<String, String> mapCopy = new LinkedHashMap<String,String>(map);
1061                    if (mapCopy.keySet().contains(null)) throw new RuntimeException("Error: Null as map key in preference setting (key '"+key+"')");
1062                    if (mapCopy.values().contains(null)) throw new RuntimeException("Error: Null as map value in preference setting (key '"+key+"')");
1063                    valueCopy.add(Collections.unmodifiableMap(mapCopy));
1064                }
1065                listOfStructsProperties.put(key, Collections.unmodifiableList(valueCopy));
1066            }
1067            try {
1068                save();
1069            } catch (IOException e) {
1070                Main.warn(tr("Failed to persist preferences to ''{0}''", getPreferenceFile().getAbsoluteFile()));
1071            }
1072        }
1073        // Call outside of synchronized section in case some listener wait for other thread that wait for preference lock
1074        firePreferenceChanged(key, new MapListSetting(oldValue), new MapListSetting(valueCopy));
1075        return true;
1076    }
1077
1078    public static boolean equalListOfStructs(Collection<Map<String, String>> a, Collection<Map<String, String>> b) {
1079        if (a == null) return b == null;
1080        if (b == null) return false;
1081        if (a.size() != b.size()) return false;
1082        Iterator<Map<String, String>> itA = a.iterator();
1083        Iterator<Map<String, String>> itB = b.iterator();
1084        while (itA.hasNext()) {
1085            if (!equalMap(itA.next(), itB.next())) return false;
1086        }
1087        return true;
1088    }
1089
1090    private static boolean equalMap(Map<String, String> a, Map<String, String> b) {
1091        if (a == null) return b == null;
1092        if (b == null) return false;
1093        if (a.size() != b.size()) return false;
1094        for (Entry<String, String> e : a.entrySet()) {
1095            if (!Utils.equal(e.getValue(), b.get(e.getKey()))) return false;
1096        }
1097        return true;
1098    }
1099
1100    synchronized private void putListOfStructsDefault(String key, List<Map<String, String>> val) {
1101        listOfStructsDefaults.put(key, val);
1102    }
1103
1104    @Retention(RetentionPolicy.RUNTIME) public @interface pref { }
1105    @Retention(RetentionPolicy.RUNTIME) public @interface writeExplicitly { }
1106
1107    /**
1108     * Get a list of hashes which are represented by a struct-like class.
1109     * Possible properties are given by fields of the class klass that have
1110     * the @pref annotation.
1111     * Default constructor is used to initialize the struct objects, properties
1112     * then override some of these default values.
1113     * @param key main preference key
1114     * @param klass The struct class
1115     * @return a list of objects of type T or an empty list if nothing was found
1116     */
1117    public <T> List<T> getListOfStructs(String key, Class<T> klass) {
1118        List<T> r = getListOfStructs(key, null, klass);
1119        if (r == null)
1120            return Collections.emptyList();
1121        else
1122            return r;
1123    }
1124
1125    /**
1126     * same as above, but returns def if nothing was found
1127     */
1128    public <T> List<T> getListOfStructs(String key, Collection<T> def, Class<T> klass) {
1129        Collection<Map<String,String>> prop =
1130            getListOfStructs(key, def == null ? null : serializeListOfStructs(def, klass));
1131        if (prop == null)
1132            return def == null ? null : new ArrayList<T>(def);
1133        List<T> lst = new ArrayList<T>();
1134        for (Map<String,String> entries : prop) {
1135            T struct = deserializeStruct(entries, klass);
1136            lst.add(struct);
1137        }
1138        return lst;
1139    }
1140
1141    /**
1142     * Save a list of hashes represented by a struct-like class.
1143     * Considers only fields that have the @pref annotation.
1144     * In addition it does not write fields with null values. (Thus they are cleared)
1145     * Default values are given by the field values after default constructor has
1146     * been called.
1147     * Fields equal to the default value are not written unless the field has
1148     * the @writeExplicitly annotation.
1149     * @param key main preference key
1150     * @param val the list that is supposed to be saved
1151     * @param klass The struct class
1152     * @return true if something has changed
1153     */
1154    public <T> boolean putListOfStructs(String key, Collection<T> val, Class<T> klass) {
1155        return putListOfStructs(key, serializeListOfStructs(val, klass));
1156    }
1157
1158    private <T> Collection<Map<String,String>> serializeListOfStructs(Collection<T> l, Class<T> klass) {
1159        if (l == null)
1160            return null;
1161        Collection<Map<String,String>> vals = new ArrayList<Map<String,String>>();
1162        for (T struct : l) {
1163            if (struct == null) {
1164                continue;
1165            }
1166            vals.add(serializeStruct(struct, klass));
1167        }
1168        return vals;
1169    }
1170
1171    public static <T> Map<String,String> serializeStruct(T struct, Class<T> klass) {
1172        T structPrototype;
1173        try {
1174            structPrototype = klass.newInstance();
1175        } catch (InstantiationException ex) {
1176            throw new RuntimeException(ex);
1177        } catch (IllegalAccessException ex) {
1178            throw new RuntimeException(ex);
1179        }
1180
1181        Map<String,String> hash = new LinkedHashMap<String,String>();
1182        for (Field f : klass.getDeclaredFields()) {
1183            if (f.getAnnotation(pref.class) == null) {
1184                continue;
1185            }
1186            f.setAccessible(true);
1187            try {
1188                Object fieldValue = f.get(struct);
1189                Object defaultFieldValue = f.get(structPrototype);
1190                if (fieldValue != null) {
1191                    if (f.getAnnotation(writeExplicitly.class) != null || !Utils.equal(fieldValue, defaultFieldValue)) {
1192                        hash.put(f.getName().replace("_", "-"), fieldValue.toString());
1193                    }
1194                }
1195            } catch (IllegalArgumentException ex) {
1196                throw new RuntimeException();
1197            } catch (IllegalAccessException ex) {
1198                throw new RuntimeException();
1199            }
1200        }
1201        return hash;
1202    }
1203
1204    public static <T> T deserializeStruct(Map<String,String> hash, Class<T> klass) {
1205        T struct = null;
1206        try {
1207            struct = klass.newInstance();
1208        } catch (InstantiationException ex) {
1209            throw new RuntimeException();
1210        } catch (IllegalAccessException ex) {
1211            throw new RuntimeException();
1212        }
1213        for (Entry<String,String> key_value : hash.entrySet()) {
1214            Object value = null;
1215            Field f;
1216            try {
1217                f = klass.getDeclaredField(key_value.getKey().replace("-", "_"));
1218            } catch (NoSuchFieldException ex) {
1219                continue;
1220            } catch (SecurityException ex) {
1221                throw new RuntimeException();
1222            }
1223            if (f.getAnnotation(pref.class) == null) {
1224                continue;
1225            }
1226            f.setAccessible(true);
1227            if (f.getType() == Boolean.class || f.getType() == boolean.class) {
1228                value = Boolean.parseBoolean(key_value.getValue());
1229            } else if (f.getType() == Integer.class || f.getType() == int.class) {
1230                try {
1231                    value = Integer.parseInt(key_value.getValue());
1232                } catch (NumberFormatException nfe) {
1233                    continue;
1234                }
1235            } else if (f.getType() == Double.class || f.getType() == double.class) {
1236                try {
1237                    value = Double.parseDouble(key_value.getValue());
1238                } catch (NumberFormatException nfe) {
1239                    continue;
1240                }
1241            } else  if (f.getType() == String.class) {
1242                value = key_value.getValue();
1243            } else
1244                throw new RuntimeException("unsupported preference primitive type");
1245
1246            try {
1247                f.set(struct, value);
1248            } catch (IllegalArgumentException ex) {
1249                throw new AssertionError();
1250            } catch (IllegalAccessException ex) {
1251                throw new RuntimeException();
1252            }
1253        }
1254        return struct;
1255    }
1256
1257    public boolean putSetting(final String key, Setting value) {
1258        if (value == null) return false;
1259        class PutVisitor implements SettingVisitor {
1260            public boolean changed;
1261            @Override
1262            public void visit(StringSetting setting) {
1263                changed = put(key, setting.getValue());
1264            }
1265            @Override
1266            public void visit(ListSetting setting) {
1267                changed = putCollection(key, setting.getValue());
1268            }
1269            @Override
1270            public void visit(ListListSetting setting) {
1271                @SuppressWarnings("unchecked")
1272                boolean changed = putArray(key, (Collection) setting.getValue());
1273                this.changed = changed;
1274            }
1275            @Override
1276            public void visit(MapListSetting setting) {
1277                changed = putListOfStructs(key, setting.getValue());
1278            }
1279        }
1280        PutVisitor putVisitor = new PutVisitor();
1281        value.visit(putVisitor);
1282        return putVisitor.changed;
1283    }
1284
1285    public Map<String, Setting> getAllSettings() {
1286        Map<String, Setting> settings = new TreeMap<String, Setting>();
1287
1288        for (Entry<String, String> e : properties.entrySet()) {
1289            settings.put(e.getKey(), new StringSetting(e.getValue()));
1290        }
1291        for (Entry<String, List<String>> e : collectionProperties.entrySet()) {
1292            settings.put(e.getKey(), new ListSetting(e.getValue()));
1293        }
1294        for (Entry<String, List<List<String>>> e : arrayProperties.entrySet()) {
1295            settings.put(e.getKey(), new ListListSetting(e.getValue()));
1296        }
1297        for (Entry<String, List<Map<String, String>>> e : listOfStructsProperties.entrySet()) {
1298            settings.put(e.getKey(), new MapListSetting(e.getValue()));
1299        }
1300        return settings;
1301    }
1302
1303    public Map<String, Setting> getAllDefaults() {
1304        Map<String, Setting> allDefaults = new TreeMap<String, Setting>();
1305
1306        for (Entry<String, String> e : defaults.entrySet()) {
1307            allDefaults.put(e.getKey(), new StringSetting(e.getValue()));
1308        }
1309        for (Entry<String, List<String>> e : collectionDefaults.entrySet()) {
1310            allDefaults.put(e.getKey(), new ListSetting(e.getValue()));
1311        }
1312        for (Entry<String, List<List<String>>> e : arrayDefaults.entrySet()) {
1313            allDefaults.put(e.getKey(), new ListListSetting(e.getValue()));
1314        }
1315        for (Entry<String, List<Map<String, String>>> e : listOfStructsDefaults.entrySet()) {
1316            allDefaults.put(e.getKey(), new MapListSetting(e.getValue()));
1317        }
1318        return allDefaults;
1319    }
1320
1321    /**
1322     * Updates system properties with the current values in the preferences.
1323     *
1324     */
1325    public void updateSystemProperties() {
1326        if(getBoolean("prefer.ipv6", false)) {
1327            // never set this to false, only true!
1328            updateSystemProperty("java.net.preferIPv6Addresses", "true");
1329        }
1330        updateSystemProperty("http.agent", Version.getInstance().getAgentString());
1331        updateSystemProperty("user.language", get("language"));
1332        // Workaround to fix a Java bug.
1333        // Force AWT toolkit to update its internal preferences (fix #3645).
1334        // This ugly hack comes from Sun bug database: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6292739
1335        try {
1336            Field field = Toolkit.class.getDeclaredField("resources");
1337            field.setAccessible(true);
1338            field.set(null, ResourceBundle.getBundle("sun.awt.resources.awt"));
1339        } catch (Exception e) {
1340            // Ignore all exceptions
1341        }
1342        // Workaround to fix another Java bug
1343        // Force Java 7 to use old sorting algorithm of Arrays.sort (fix #8712).
1344        // See Oracle bug database: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=7075600
1345        // and http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6923200
1346        if (Main.pref.getBoolean("jdk.Arrays.useLegacyMergeSort", !Version.getInstance().isLocalBuild())) {
1347            updateSystemProperty("java.util.Arrays.useLegacyMergeSort", "true");
1348        }
1349    }
1350
1351    private void updateSystemProperty(String key, String value) {
1352        if (value != null) {
1353            System.setProperty(key, value);
1354        }
1355    }
1356
1357    /**
1358     * The default plugin site
1359     */
1360    private final static String[] DEFAULT_PLUGIN_SITE = {
1361    Main.JOSM_WEBSITE+"/plugin%<?plugins=>"};
1362
1363    /**
1364     * Replies the collection of plugin site URLs from where plugin lists can be downloaded
1365     */
1366    public Collection<String> getPluginSites() {
1367        return getCollection("pluginmanager.sites", Arrays.asList(DEFAULT_PLUGIN_SITE));
1368    }
1369
1370    /**
1371     * Sets the collection of plugin site URLs.
1372     *
1373     * @param sites the site URLs
1374     */
1375    public void setPluginSites(Collection<String> sites) {
1376        putCollection("pluginmanager.sites", sites);
1377    }
1378
1379    protected XMLStreamReader parser;
1380
1381    public void validateXML(Reader in) throws Exception {
1382        SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
1383        Schema schema = factory.newSchema(new StreamSource(new MirroredInputStream("resource://data/preferences.xsd")));
1384        Validator validator = schema.newValidator();
1385        validator.validate(new StreamSource(in));
1386    }
1387
1388    public void fromXML(Reader in) throws XMLStreamException {
1389        XMLStreamReader parser = XMLInputFactory.newInstance().createXMLStreamReader(in);
1390        this.parser = parser;
1391        parse();
1392    }
1393
1394    public void parse() throws XMLStreamException {
1395        int event = parser.getEventType();
1396        while (true) {
1397            if (event == XMLStreamConstants.START_ELEMENT) {
1398                parseRoot();
1399            } else if (event == XMLStreamConstants.END_ELEMENT) {
1400                return;
1401            }
1402            if (parser.hasNext()) {
1403                event = parser.next();
1404            } else {
1405                break;
1406            }
1407        }
1408        parser.close();
1409    }
1410
1411    public void parseRoot() throws XMLStreamException {
1412        while (true) {
1413            int event = parser.next();
1414            if (event == XMLStreamConstants.START_ELEMENT) {
1415                if (parser.getLocalName().equals("tag")) {
1416                    properties.put(parser.getAttributeValue(null, "key"), parser.getAttributeValue(null, "value"));
1417                    jumpToEnd();
1418                } else if (parser.getLocalName().equals("list") ||
1419                        parser.getLocalName().equals("collection") ||
1420                        parser.getLocalName().equals("lists") ||
1421                        parser.getLocalName().equals("maps")
1422                ) {
1423                    parseToplevelList();
1424                } else {
1425                    throwException("Unexpected element: "+parser.getLocalName());
1426                }
1427            } else if (event == XMLStreamConstants.END_ELEMENT) {
1428                return;
1429            }
1430        }
1431    }
1432
1433    private void jumpToEnd() throws XMLStreamException {
1434        while (true) {
1435            int event = parser.next();
1436            if (event == XMLStreamConstants.START_ELEMENT) {
1437                jumpToEnd();
1438            } else if (event == XMLStreamConstants.END_ELEMENT) {
1439                return;
1440            }
1441        }
1442    }
1443
1444    protected void parseToplevelList() throws XMLStreamException {
1445        String key = parser.getAttributeValue(null, "key");
1446        String name = parser.getLocalName();
1447
1448        List<String> entries = null;
1449        List<List<String>> lists = null;
1450        List<Map<String, String>> maps = null;
1451        while (true) {
1452            int event = parser.next();
1453            if (event == XMLStreamConstants.START_ELEMENT) {
1454                if (parser.getLocalName().equals("entry")) {
1455                    if (entries == null) {
1456                        entries = new ArrayList<String>();
1457                    }
1458                    entries.add(parser.getAttributeValue(null, "value"));
1459                    jumpToEnd();
1460                } else if (parser.getLocalName().equals("list")) {
1461                    if (lists == null) {
1462                        lists = new ArrayList<List<String>>();
1463                    }
1464                    lists.add(parseInnerList());
1465                } else if (parser.getLocalName().equals("map")) {
1466                    if (maps == null) {
1467                        maps = new ArrayList<Map<String, String>>();
1468                    }
1469                    maps.add(parseMap());
1470                } else {
1471                    throwException("Unexpected element: "+parser.getLocalName());
1472                }
1473            } else if (event == XMLStreamConstants.END_ELEMENT) {
1474                break;
1475            }
1476        }
1477        if (entries != null) {
1478            collectionProperties.put(key, Collections.unmodifiableList(entries));
1479        } else if (lists != null) {
1480            arrayProperties.put(key, Collections.unmodifiableList(lists));
1481        } else if (maps != null) {
1482            listOfStructsProperties.put(key, Collections.unmodifiableList(maps));
1483        } else {
1484            if (name.equals("lists")) {
1485                arrayProperties.put(key, Collections.<List<String>>emptyList());
1486            } else if (name.equals("maps")) {
1487                listOfStructsProperties.put(key, Collections.<Map<String, String>>emptyList());
1488            } else {
1489                collectionProperties.put(key, Collections.<String>emptyList());
1490            }
1491        }
1492    }
1493
1494    protected List<String> parseInnerList() throws XMLStreamException {
1495        List<String> entries = new ArrayList<String>();
1496        while (true) {
1497            int event = parser.next();
1498            if (event == XMLStreamConstants.START_ELEMENT) {
1499                if (parser.getLocalName().equals("entry")) {
1500                    entries.add(parser.getAttributeValue(null, "value"));
1501                    jumpToEnd();
1502                } else {
1503                    throwException("Unexpected element: "+parser.getLocalName());
1504                }
1505            } else if (event == XMLStreamConstants.END_ELEMENT) {
1506                break;
1507            }
1508        }
1509        return Collections.unmodifiableList(entries);
1510    }
1511
1512    protected Map<String, String> parseMap() throws XMLStreamException {
1513        Map<String, String> map = new LinkedHashMap<String, String>();
1514        while (true) {
1515            int event = parser.next();
1516            if (event == XMLStreamConstants.START_ELEMENT) {
1517                if (parser.getLocalName().equals("tag")) {
1518                    map.put(parser.getAttributeValue(null, "key"), parser.getAttributeValue(null, "value"));
1519                    jumpToEnd();
1520                } else {
1521                    throwException("Unexpected element: "+parser.getLocalName());
1522                }
1523            } else if (event == XMLStreamConstants.END_ELEMENT) {
1524                break;
1525            }
1526        }
1527        return Collections.unmodifiableMap(map);
1528    }
1529
1530    protected void throwException(String msg) {
1531        throw new RuntimeException(msg + tr(" (at line {0}, column {1})", parser.getLocation().getLineNumber(), parser.getLocation().getColumnNumber()));
1532    }
1533
1534    private class SettingToXml implements SettingVisitor {
1535        private StringBuilder b;
1536        private boolean noPassword;
1537        private String key;
1538
1539        public SettingToXml(StringBuilder b, boolean noPassword) {
1540            this.b = b;
1541            this.noPassword = noPassword;
1542        }
1543
1544        public void setKey(String key) {
1545            this.key = key;
1546        }
1547
1548        @Override
1549        public void visit(StringSetting setting) {
1550            if (noPassword && key.equals("osm-server.password"))
1551                return; // do not store plain password.
1552            String r = setting.getValue();
1553            String s = defaults.get(key);
1554            /* don't save default values */
1555            if(s == null || !s.equals(r)) {
1556                b.append("  <tag key='");
1557                b.append(XmlWriter.encode(key));
1558                b.append("' value='");
1559                b.append(XmlWriter.encode(setting.getValue()));
1560                b.append("'/>\n");
1561            }
1562        }
1563
1564        @Override
1565        public void visit(ListSetting setting) {
1566            b.append("  <list key='").append(XmlWriter.encode(key)).append("'>\n");
1567            for (String s : setting.getValue()) {
1568                b.append("    <entry value='").append(XmlWriter.encode(s)).append("'/>\n");
1569            }
1570            b.append("  </list>\n");
1571        }
1572
1573        @Override
1574        public void visit(ListListSetting setting) {
1575            b.append("  <lists key='").append(XmlWriter.encode(key)).append("'>\n");
1576            for (List<String> list : setting.getValue()) {
1577                b.append("    <list>\n");
1578                for (String s : list) {
1579                    b.append("      <entry value='").append(XmlWriter.encode(s)).append("'/>\n");
1580                }
1581                b.append("    </list>\n");
1582            }
1583            b.append("  </lists>\n");
1584        }
1585
1586        @Override
1587        public void visit(MapListSetting setting) {
1588            b.append("  <maps key='").append(XmlWriter.encode(key)).append("'>\n");
1589            for (Map<String, String> struct : setting.getValue()) {
1590                b.append("    <map>\n");
1591                for (Entry<String, String> e : struct.entrySet()) {
1592                    b.append("      <tag key='").append(XmlWriter.encode(e.getKey())).append("' value='").append(XmlWriter.encode(e.getValue())).append("'/>\n");
1593                }
1594                b.append("    </map>\n");
1595            }
1596            b.append("  </maps>\n");
1597        }
1598    }
1599
1600    public String toXML(boolean nopass) {
1601        StringBuilder b = new StringBuilder(
1602                "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
1603                "<preferences xmlns=\""+Main.JOSM_WEBSITE+"/preferences-1.0\" version=\""+
1604                Version.getInstance().getVersion() + "\">\n");
1605        SettingToXml toXml = new SettingToXml(b, nopass);
1606        Map<String, Setting<?>> settings = new TreeMap<String, Setting<?>>();
1607
1608        for (Entry<String, String> e : properties.entrySet()) {
1609            settings.put(e.getKey(), new StringSetting(e.getValue()));
1610        }
1611        for (Entry<String, List<String>> e : collectionProperties.entrySet()) {
1612            settings.put(e.getKey(), new ListSetting(e.getValue()));
1613        }
1614        for (Entry<String, List<List<String>>> e : arrayProperties.entrySet()) {
1615            settings.put(e.getKey(), new ListListSetting(e.getValue()));
1616        }
1617        for (Entry<String, List<Map<String, String>>> e : listOfStructsProperties.entrySet()) {
1618            settings.put(e.getKey(), new MapListSetting(e.getValue()));
1619        }
1620        for (Entry<String, Setting<?>> e : settings.entrySet()) {
1621            toXml.setKey(e.getKey());
1622            e.getValue().visit(toXml);
1623        }
1624        b.append("</preferences>\n");
1625        return b.toString();
1626    }
1627
1628    /**
1629     * Removes obsolete preference settings. If you throw out a once-used preference
1630     * setting, add it to the list here with an expiry date (written as comment). If you
1631     * see something with an expiry date in the past, remove it from the list.
1632     */
1633    public void removeObsolete() {
1634        /* update the data with old consumer key*/
1635        if(getInteger("josm.version", Version.getInstance().getVersion()) < 6076) {
1636            if(!get("oauth.access-token.key").isEmpty() && get("oauth.settings.consumer-key").isEmpty()) {
1637                put("oauth.settings.consumer-key", "AdCRxTpvnbmfV8aPqrTLyA");
1638                put("oauth.settings.consumer-secret", "XmYOiGY9hApytcBC3xCec3e28QBqOWz5g6DSb5UpE");
1639            }
1640        }
1641
1642        String[] obsolete = {
1643                "downloadAlong.downloadAlongTrack.distance",   // 07/2013 - can be removed mid-2014. Replaced by downloadAlongWay.distance
1644                "downloadAlong.downloadAlongTrack.area",       // 07/2013 - can be removed mid-2014. Replaced by downloadAlongWay.area
1645                "gpxLayer.downloadAlongTrack.distance",        // 07/2013 - can be removed mid-2014. Replaced by downloadAlongTrack.distance
1646                "gpxLayer.downloadAlongTrack.area",            // 07/2013 - can be removed mid-2014. Replaced by downloadAlongTrack.area
1647                "gpxLayer.downloadAlongTrack.near",            // 07/2013 - can be removed mid-2014. Replaced by downloadAlongTrack.near
1648        };
1649        for (String key : obsolete) {
1650            boolean removed = false;
1651            if (properties.containsKey(key)) { properties.remove(key); removed = true; }
1652            if (collectionProperties.containsKey(key)) { collectionProperties.remove(key); removed = true; }
1653            if (arrayProperties.containsKey(key)) { arrayProperties.remove(key); removed = true; }
1654            if (listOfStructsProperties.containsKey(key)) { listOfStructsProperties.remove(key); removed = true; }
1655            if (removed) {
1656                Main.info(tr("Preference setting {0} has been removed since it is no longer used.", key));
1657            }
1658        }
1659    }
1660
1661    public static boolean isEqual(Setting<?> a, Setting<?> b) {
1662        if (a==null && b==null) return true;
1663        if (a==null) return false;
1664        if (b==null) return false;
1665        if (a==b) return true;
1666
1667        if (a instanceof StringSetting)
1668            return (a.getValue().equals(b.getValue()));
1669        if (a instanceof ListSetting) {
1670            @SuppressWarnings("unchecked") Collection<String> aValue = (Collection<String>) a.getValue();
1671            @SuppressWarnings("unchecked") Collection<String> bValue = (Collection<String>) b.getValue();
1672            return equalCollection(aValue, bValue);
1673        }
1674        if (a instanceof ListListSetting) {
1675            @SuppressWarnings("unchecked") Collection<Collection<String>> aValue = (Collection<Collection<String>>) a.getValue();
1676            @SuppressWarnings("unchecked") Collection<List<String>> bValue = (Collection<List<String>>) b.getValue();
1677            return equalArray(aValue, bValue);
1678        }
1679        if (a instanceof MapListSetting) {
1680            @SuppressWarnings("unchecked") Collection<Map<String, String>> aValue = (Collection<Map<String, String>>) a.getValue();
1681            @SuppressWarnings("unchecked") Collection<Map<String, String>> bValue = (Collection<Map<String, String>>) b.getValue();
1682            return equalListOfStructs(aValue, bValue);
1683        }
1684        return a.equals(b);
1685    }
1686
1687}