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