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