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