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.File;
011import java.io.FileOutputStream;
012import java.io.IOException;
013import java.io.OutputStreamWriter;
014import java.io.PrintWriter;
015import java.io.Reader;
016import java.io.StringReader;
017import java.io.StringWriter;
018import java.lang.annotation.Retention;
019import java.lang.annotation.RetentionPolicy;
020import java.lang.reflect.Field;
021import java.nio.charset.StandardCharsets;
022import java.util.ArrayList;
023import java.util.Collection;
024import java.util.Collections;
025import java.util.HashMap;
026import java.util.HashSet;
027import java.util.Iterator;
028import java.util.LinkedHashMap;
029import java.util.LinkedList;
030import java.util.List;
031import java.util.Map;
032import java.util.Map.Entry;
033import java.util.Objects;
034import java.util.ResourceBundle;
035import java.util.Set;
036import java.util.SortedMap;
037import java.util.TreeMap;
038import java.util.concurrent.CopyOnWriteArrayList;
039import java.util.regex.Matcher;
040import java.util.regex.Pattern;
041
042import javax.json.Json;
043import javax.json.JsonArray;
044import javax.json.JsonArrayBuilder;
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.xml.stream.XMLStreamException;
053
054import org.openstreetmap.josm.Main;
055import org.openstreetmap.josm.data.preferences.ColorProperty;
056import org.openstreetmap.josm.data.preferences.ListListSetting;
057import org.openstreetmap.josm.data.preferences.ListSetting;
058import org.openstreetmap.josm.data.preferences.MapListSetting;
059import org.openstreetmap.josm.data.preferences.PreferencesReader;
060import org.openstreetmap.josm.data.preferences.PreferencesWriter;
061import org.openstreetmap.josm.data.preferences.Setting;
062import org.openstreetmap.josm.data.preferences.StringSetting;
063import org.openstreetmap.josm.io.OfflineAccessException;
064import org.openstreetmap.josm.io.OnlineResource;
065import org.openstreetmap.josm.tools.CheckParameterUtil;
066import org.openstreetmap.josm.tools.ColorHelper;
067import org.openstreetmap.josm.tools.FilteredCollection;
068import org.openstreetmap.josm.tools.I18n;
069import org.openstreetmap.josm.tools.MultiMap;
070import org.openstreetmap.josm.tools.Predicate;
071import org.openstreetmap.josm.tools.Utils;
072import org.xml.sax.SAXException;
073
074/**
075 * This class holds all preferences for JOSM.
076 *
077 * Other classes can register their beloved properties here. All properties will be
078 * saved upon set-access.
079 *
080 * Each property is a key=setting pair, where key is a String and setting can be one of
081 * 4 types:
082 *     string, list, list of lists and list of maps.
083 * In addition, each key has a unique default value that is set when the value is first
084 * accessed using one of the get...() methods. You can use the same preference
085 * key in different parts of the code, but the default value must be the same
086 * everywhere. A default value of null means, the setting has been requested, but
087 * no default value was set. This is used in advanced preferences to present a list
088 * off all possible settings.
089 *
090 * At the moment, you cannot put the empty string for string properties.
091 * put(key, "") means, the property is removed.
092 *
093 * @author imi
094 * @since 74
095 */
096public class Preferences {
097
098    private static final String[] OBSOLETE_PREF_KEYS = {
099    };
100
101    private static final long MAX_AGE_DEFAULT_PREFERENCES = 60 * 60 * 24 * 50; // 50 days (in seconds)
102
103    /**
104     * Internal storage for the preference directory.
105     * Do not access this variable directly!
106     * @see #getPreferencesDirectory()
107     */
108    private File preferencesDir;
109
110    /**
111     * Internal storage for the cache directory.
112     */
113    private File cacheDir;
114
115    /**
116     * Internal storage for the user data directory.
117     */
118    private File userdataDir;
119
120    /**
121     * Determines if preferences file is saved each time a property is changed.
122     */
123    private boolean saveOnPut = true;
124
125    /**
126     * Maps the setting name to the current value of the setting.
127     * The map must not contain null as key or value. The mapped setting objects
128     * must not have a null value.
129     */
130    protected final SortedMap<String, Setting<?>> settingsMap = new TreeMap<>();
131
132    /**
133     * Maps the setting name to the default value of the setting.
134     * The map must not contain null as key or value. The value of the mapped
135     * setting objects can be null.
136     */
137    protected final SortedMap<String, Setting<?>> defaultsMap = new TreeMap<>();
138
139    private final Predicate<Entry<String, Setting<?>>> NO_DEFAULT_SETTINGS_ENTRY = new Predicate<Entry<String, Setting<?>>>() {
140        @Override
141        public boolean evaluate(Entry<String, Setting<?>> e) {
142            return !e.getValue().equals(defaultsMap.get(e.getKey()));
143        }
144    };
145
146    /**
147     * Maps color keys to human readable color name
148     */
149    protected final SortedMap<String, String> colornames = new TreeMap<>();
150
151    /**
152     * Indicates whether {@link #init(boolean)} completed successfully.
153     * Used to decide whether to write backup preference file in {@link #save()}
154     */
155    protected boolean initSuccessful;
156
157    /**
158     * Event triggered when a preference entry value changes.
159     */
160    public interface PreferenceChangeEvent {
161        /**
162         * Returns the preference key.
163         * @return the preference key
164         */
165        String getKey();
166
167        /**
168         * Returns the old preference value.
169         * @return the old preference value
170         */
171        Setting<?> getOldValue();
172
173        /**
174         * Returns the new preference value.
175         * @return the new preference value
176         */
177        Setting<?> getNewValue();
178    }
179
180    /**
181     * Listener to preference change events.
182     */
183    public interface PreferenceChangedListener {
184        /**
185         * Trigerred when a preference entry value changes.
186         * @param e the preference change event
187         */
188        void preferenceChanged(PreferenceChangeEvent e);
189    }
190
191    private static class DefaultPreferenceChangeEvent implements PreferenceChangeEvent {
192        private final String key;
193        private final Setting<?> oldValue;
194        private final Setting<?> newValue;
195
196        DefaultPreferenceChangeEvent(String key, Setting<?> oldValue, Setting<?> newValue) {
197            this.key = key;
198            this.oldValue = oldValue;
199            this.newValue = newValue;
200        }
201
202        @Override
203        public String getKey() {
204            return key;
205        }
206
207        @Override
208        public Setting<?> getOldValue() {
209            return oldValue;
210        }
211
212        @Override
213        public Setting<?> getNewValue() {
214            return newValue;
215        }
216    }
217
218    public interface ColorKey {
219        String getColorName();
220
221        String getSpecialName();
222
223        Color getDefaultValue();
224    }
225
226    private final CopyOnWriteArrayList<PreferenceChangedListener> listeners = new CopyOnWriteArrayList<>();
227
228    /**
229     * Adds a new preferences listener.
230     * @param listener The listener to add
231     */
232    public void addPreferenceChangeListener(PreferenceChangedListener listener) {
233        if (listener != null) {
234            listeners.addIfAbsent(listener);
235        }
236    }
237
238    /**
239     * Removes a preferences listener.
240     * @param listener The listener to remove
241     */
242    public void removePreferenceChangeListener(PreferenceChangedListener listener) {
243        listeners.remove(listener);
244    }
245
246    protected void firePreferenceChanged(String key, Setting<?> oldValue, Setting<?> newValue) {
247        PreferenceChangeEvent evt = new DefaultPreferenceChangeEvent(key, oldValue, newValue);
248        for (PreferenceChangedListener l : listeners) {
249            l.preferenceChanged(evt);
250        }
251    }
252
253    /**
254     * Returns the user defined preferences directory, containing the preferences.xml file
255     * @return The user defined preferences directory, containing the preferences.xml file
256     * @since 7834
257     */
258    public File getPreferencesDirectory() {
259        if (preferencesDir != null)
260            return preferencesDir;
261        String path;
262        path = System.getProperty("josm.pref");
263        if (path != null) {
264            preferencesDir = new File(path).getAbsoluteFile();
265        } else {
266            path = System.getProperty("josm.home");
267            if (path != null) {
268                preferencesDir = new File(path).getAbsoluteFile();
269            } else {
270                preferencesDir = Main.platform.getDefaultPrefDirectory();
271            }
272        }
273        return preferencesDir;
274    }
275
276    /**
277     * Returns the user data directory, containing autosave, plugins, etc.
278     * Depending on the OS it may be the same directory as preferences directory.
279     * @return The user data directory, containing autosave, plugins, etc.
280     * @since 7834
281     */
282    public File getUserDataDirectory() {
283        if (userdataDir != null)
284            return userdataDir;
285        String path;
286        path = System.getProperty("josm.userdata");
287        if (path != null) {
288            userdataDir = new File(path).getAbsoluteFile();
289        } else {
290            path = System.getProperty("josm.home");
291            if (path != null) {
292                userdataDir = new File(path).getAbsoluteFile();
293            } else {
294                userdataDir = Main.platform.getDefaultUserDataDirectory();
295            }
296        }
297        return userdataDir;
298    }
299
300    /**
301     * Returns the user preferences file (preferences.xml).
302     * @return The user preferences file (preferences.xml)
303     */
304    public File getPreferenceFile() {
305        return new File(getPreferencesDirectory(), "preferences.xml");
306    }
307
308    /**
309     * Returns the cache file for default preferences.
310     * @return the cache file for default preferences
311     */
312    public File getDefaultsCacheFile() {
313        return new File(getCacheDirectory(), "default_preferences.xml");
314    }
315
316    /**
317     * Returns the user plugin directory.
318     * @return The user plugin directory
319     */
320    public File getPluginsDirectory() {
321        return new File(getUserDataDirectory(), "plugins");
322    }
323
324    /**
325     * Get the directory where cached content of any kind should be stored.
326     *
327     * If the directory doesn't exist on the file system, it will be created by this method.
328     *
329     * @return the cache directory
330     */
331    public File getCacheDirectory() {
332        if (cacheDir != null)
333            return cacheDir;
334        String path = System.getProperty("josm.cache");
335        if (path != null) {
336            cacheDir = new File(path).getAbsoluteFile();
337        } else {
338            path = System.getProperty("josm.home");
339            if (path != null) {
340                cacheDir = new File(path, "cache");
341            } else {
342                path = get("cache.folder", null);
343                if (path != null) {
344                    cacheDir = new File(path).getAbsoluteFile();
345                } else {
346                    cacheDir = Main.platform.getDefaultCacheDirectory();
347                }
348            }
349        }
350        if (!cacheDir.exists() && !cacheDir.mkdirs()) {
351            Main.warn(tr("Failed to create missing cache directory: {0}", cacheDir.getAbsoluteFile()));
352            JOptionPane.showMessageDialog(
353                    Main.parent,
354                    tr("<html>Failed to create missing cache directory: {0}</html>", cacheDir.getAbsoluteFile()),
355                    tr("Error"),
356                    JOptionPane.ERROR_MESSAGE
357            );
358        }
359        return cacheDir;
360    }
361
362    private static void addPossibleResourceDir(Set<String> locations, String s) {
363        if (s != null) {
364            if (!s.endsWith(File.separator)) {
365                s += File.separator;
366            }
367            locations.add(s);
368        }
369    }
370
371    /**
372     * Returns a set of all existing directories where resources could be stored.
373     * @return A set of all existing directories where resources could be stored.
374     */
375    public Collection<String> getAllPossiblePreferenceDirs() {
376        Set<String> locations = new HashSet<>();
377        addPossibleResourceDir(locations, getPreferencesDirectory().getPath());
378        addPossibleResourceDir(locations, getUserDataDirectory().getPath());
379        addPossibleResourceDir(locations, System.getenv("JOSM_RESOURCES"));
380        addPossibleResourceDir(locations, System.getProperty("josm.resources"));
381        if (Main.isPlatformWindows()) {
382            String appdata = System.getenv("APPDATA");
383            if (System.getenv("ALLUSERSPROFILE") != null && appdata != null
384                    && appdata.lastIndexOf(File.separator) != -1) {
385                appdata = appdata.substring(appdata.lastIndexOf(File.separator));
386                locations.add(new File(new File(System.getenv("ALLUSERSPROFILE"),
387                        appdata), "JOSM").getPath());
388            }
389        } else {
390            locations.add("/usr/local/share/josm/");
391            locations.add("/usr/local/lib/josm/");
392            locations.add("/usr/share/josm/");
393            locations.add("/usr/lib/josm/");
394        }
395        return locations;
396    }
397
398    /**
399     * Get settings value for a certain key.
400     * @param key the identifier for the setting
401     * @return "" if there is nothing set for the preference key, the corresponding value otherwise. The result is not null.
402     */
403    public synchronized String get(final String key) {
404        String value = get(key, null);
405        return value == null ? "" : value;
406    }
407
408    /**
409     * Get settings value for a certain key and provide default a value.
410     * @param key the identifier for the setting
411     * @param def the default value. For each call of get() with a given key, the default value must be the same.
412     * @return the corresponding value if the property has been set before, {@code def} otherwise
413     */
414    public synchronized String get(final String key, final String def) {
415        return getSetting(key, new StringSetting(def), StringSetting.class).getValue();
416    }
417
418    public synchronized Map<String, String> getAllPrefix(final String prefix) {
419        final Map<String, String> all = new TreeMap<>();
420        for (final Entry<String, Setting<?>> e : settingsMap.entrySet()) {
421            if (e.getKey().startsWith(prefix) && (e.getValue() instanceof StringSetting)) {
422                all.put(e.getKey(), ((StringSetting) e.getValue()).getValue());
423            }
424        }
425        return all;
426    }
427
428    public synchronized List<String> getAllPrefixCollectionKeys(final String prefix) {
429        final List<String> all = new LinkedList<>();
430        for (Map.Entry<String, Setting<?>> entry : settingsMap.entrySet()) {
431            if (entry.getKey().startsWith(prefix) && entry.getValue() instanceof ListSetting) {
432                all.add(entry.getKey());
433            }
434        }
435        return all;
436    }
437
438    public synchronized Map<String, String> getAllColors() {
439        final Map<String, String> all = new TreeMap<>();
440        for (final Entry<String, Setting<?>> e : defaultsMap.entrySet()) {
441            if (e.getKey().startsWith("color.") && e.getValue() instanceof StringSetting) {
442                StringSetting d = (StringSetting) e.getValue();
443                if (d.getValue() != null) {
444                    all.put(e.getKey().substring(6), d.getValue());
445                }
446            }
447        }
448        for (final Entry<String, Setting<?>> e : settingsMap.entrySet()) {
449            if (e.getKey().startsWith("color.") && (e.getValue() instanceof StringSetting)) {
450                all.put(e.getKey().substring(6), ((StringSetting) e.getValue()).getValue());
451            }
452        }
453        return all;
454    }
455
456    public synchronized boolean getBoolean(final String key) {
457        String s = get(key, null);
458        return s != null && Boolean.parseBoolean(s);
459    }
460
461    public synchronized boolean getBoolean(final String key, final boolean def) {
462        return Boolean.parseBoolean(get(key, Boolean.toString(def)));
463    }
464
465    public synchronized boolean getBoolean(final String key, final String specName, final boolean def) {
466        boolean generic = getBoolean(key, def);
467        String skey = key+'.'+specName;
468        Setting<?> prop = settingsMap.get(skey);
469        if (prop instanceof StringSetting)
470            return Boolean.parseBoolean(((StringSetting) prop).getValue());
471        else
472            return generic;
473    }
474
475    /**
476     * Set a value for a certain setting.
477     * @param key the unique identifier for the setting
478     * @param value the value of the setting. Can be null or "" which both removes the key-value entry.
479     * @return {@code true}, if something has changed (i.e. value is different than before)
480     */
481    public boolean put(final String key, String value) {
482        if (value != null && value.isEmpty()) {
483            value = null;
484        }
485        return putSetting(key, value == null ? null : new StringSetting(value));
486    }
487
488    public boolean put(final String key, final boolean value) {
489        return put(key, Boolean.toString(value));
490    }
491
492    public boolean putInteger(final String key, final Integer value) {
493        return put(key, Integer.toString(value));
494    }
495
496    public boolean putDouble(final String key, final Double value) {
497        return put(key, Double.toString(value));
498    }
499
500    public boolean putLong(final String key, final Long value) {
501        return put(key, Long.toString(value));
502    }
503
504    /**
505     * Called after every put. In case of a problem, do nothing but output the error in log.
506     * @throws IOException if any I/O error occurs
507     */
508    public void save() throws IOException {
509        save(getPreferenceFile(),
510                new FilteredCollection<>(settingsMap.entrySet(), NO_DEFAULT_SETTINGS_ENTRY), false);
511    }
512
513    public void saveDefaults() throws IOException {
514        save(getDefaultsCacheFile(), defaultsMap.entrySet(), true);
515    }
516
517    public void save(File prefFile, Collection<Entry<String, Setting<?>>> settings, boolean defaults) throws IOException {
518
519        if (!defaults) {
520            /* currently unused, but may help to fix configuration issues in future */
521            putInteger("josm.version", Version.getInstance().getVersion());
522
523            updateSystemProperties();
524        }
525
526        File backupFile = new File(prefFile + "_backup");
527
528        // Backup old preferences if there are old preferences
529        if (prefFile.exists() && prefFile.length() > 0 && initSuccessful) {
530            Utils.copyFile(prefFile, backupFile);
531        }
532
533        try (PrintWriter out = new PrintWriter(new OutputStreamWriter(
534                new FileOutputStream(prefFile + "_tmp"), StandardCharsets.UTF_8), false)) {
535            PreferencesWriter writer = new PreferencesWriter(out, false, defaults);
536            writer.write(settings);
537        }
538
539        File tmpFile = new File(prefFile + "_tmp");
540        Utils.copyFile(tmpFile, prefFile);
541        Utils.deleteFile(tmpFile, marktr("Unable to delete temporary file {0}"));
542
543        setCorrectPermissions(prefFile);
544        setCorrectPermissions(backupFile);
545    }
546
547    private static void setCorrectPermissions(File file) {
548        if (!file.setReadable(false, false) && Main.isDebugEnabled()) {
549            Main.debug(tr("Unable to set file non-readable {0}", file.getAbsolutePath()));
550        }
551        if (!file.setWritable(false, false) && Main.isDebugEnabled()) {
552            Main.debug(tr("Unable to set file non-writable {0}", file.getAbsolutePath()));
553        }
554        if (!file.setExecutable(false, false) && Main.isDebugEnabled()) {
555            Main.debug(tr("Unable to set file non-executable {0}", file.getAbsolutePath()));
556        }
557        if (!file.setReadable(true, true) && Main.isDebugEnabled()) {
558            Main.debug(tr("Unable to set file readable {0}", file.getAbsolutePath()));
559        }
560        if (!file.setWritable(true, true) && Main.isDebugEnabled()) {
561            Main.debug(tr("Unable to set file writable {0}", file.getAbsolutePath()));
562        }
563    }
564
565    /**
566     * Loads preferences from settings file.
567     * @throws IOException if any I/O error occurs while reading the file
568     * @throws SAXException if the settings file does not contain valid XML
569     * @throws XMLStreamException if an XML error occurs while parsing the file (after validation)
570     */
571    protected void load() throws IOException, SAXException, XMLStreamException {
572        File pref = getPreferenceFile();
573        PreferencesReader.validateXML(pref);
574        PreferencesReader reader = new PreferencesReader(pref, false);
575        reader.parse();
576        settingsMap.clear();
577        settingsMap.putAll(reader.getSettings());
578        updateSystemProperties();
579        removeObsolete(reader.getVersion());
580    }
581
582    /**
583     * Loads default preferences from default settings cache file.
584     *
585     * Discards entries older than {@link #MAX_AGE_DEFAULT_PREFERENCES}.
586     *
587     * @throws IOException if any I/O error occurs while reading the file
588     * @throws SAXException if the settings file does not contain valid XML
589     * @throws XMLStreamException if an XML error occurs while parsing the file (after validation)
590     */
591    protected void loadDefaults() throws IOException, XMLStreamException, SAXException {
592        File def = getDefaultsCacheFile();
593        PreferencesReader.validateXML(def);
594        PreferencesReader reader = new PreferencesReader(def, true);
595        reader.parse();
596        defaultsMap.clear();
597        long minTime = System.currentTimeMillis() / 1000 - MAX_AGE_DEFAULT_PREFERENCES;
598        for (Entry<String, Setting<?>> e : reader.getSettings().entrySet()) {
599            if (e.getValue().getTime() >= minTime) {
600                defaultsMap.put(e.getKey(), e.getValue());
601            }
602        }
603    }
604
605    /**
606     * Loads preferences from XML reader.
607     * @param in XML reader
608     * @throws XMLStreamException if any XML stream error occurs
609     * @throws IOException if any I/O error occurs
610     */
611    public void fromXML(Reader in) throws XMLStreamException, IOException {
612        PreferencesReader reader = new PreferencesReader(in, false);
613        reader.parse();
614        settingsMap.clear();
615        settingsMap.putAll(reader.getSettings());
616    }
617
618    /**
619     * Initializes preferences.
620     * @param reset if {@code true}, current settings file is replaced by the default one
621     */
622    public void init(boolean reset) {
623        initSuccessful = false;
624        // get the preferences.
625        File prefDir = getPreferencesDirectory();
626        if (prefDir.exists()) {
627            if (!prefDir.isDirectory()) {
628                Main.warn(tr("Failed to initialize preferences. Preference directory ''{0}'' is not a directory.",
629                        prefDir.getAbsoluteFile()));
630                JOptionPane.showMessageDialog(
631                        Main.parent,
632                        tr("<html>Failed to initialize preferences.<br>Preference directory ''{0}'' is not a directory.</html>",
633                                prefDir.getAbsoluteFile()),
634                        tr("Error"),
635                        JOptionPane.ERROR_MESSAGE
636                );
637                return;
638            }
639        } else {
640            if (!prefDir.mkdirs()) {
641                Main.warn(tr("Failed to initialize preferences. Failed to create missing preference directory: {0}",
642                        prefDir.getAbsoluteFile()));
643                JOptionPane.showMessageDialog(
644                        Main.parent,
645                        tr("<html>Failed to initialize preferences.<br>Failed to create missing preference directory: {0}</html>",
646                                prefDir.getAbsoluteFile()),
647                        tr("Error"),
648                        JOptionPane.ERROR_MESSAGE
649                );
650                return;
651            }
652        }
653
654        File preferenceFile = getPreferenceFile();
655        try {
656            if (!preferenceFile.exists()) {
657                Main.info(tr("Missing preference file ''{0}''. Creating a default preference file.", preferenceFile.getAbsoluteFile()));
658                resetToDefault();
659                save();
660            } else if (reset) {
661                File backupFile = new File(prefDir, "preferences.xml.bak");
662                Main.platform.rename(preferenceFile, backupFile);
663                Main.warn(tr("Replacing existing preference file ''{0}'' with default preference file.", preferenceFile.getAbsoluteFile()));
664                resetToDefault();
665                save();
666            }
667        } catch (IOException e) {
668            Main.error(e);
669            JOptionPane.showMessageDialog(
670                    Main.parent,
671                    tr("<html>Failed to initialize preferences.<br>Failed to reset preference file to default: {0}</html>",
672                            getPreferenceFile().getAbsoluteFile()),
673                    tr("Error"),
674                    JOptionPane.ERROR_MESSAGE
675            );
676            return;
677        }
678        try {
679            load();
680            initSuccessful = true;
681        } catch (IOException | SAXException | XMLStreamException e) {
682            Main.error(e);
683            File backupFile = new File(prefDir, "preferences.xml.bak");
684            JOptionPane.showMessageDialog(
685                    Main.parent,
686                    tr("<html>Preferences file had errors.<br> Making backup of old one to <br>{0}<br> " +
687                            "and creating a new default preference file.</html>",
688                            backupFile.getAbsoluteFile()),
689                    tr("Error"),
690                    JOptionPane.ERROR_MESSAGE
691            );
692            Main.platform.rename(preferenceFile, backupFile);
693            try {
694                resetToDefault();
695                save();
696            } catch (IOException e1) {
697                Main.error(e1);
698                Main.warn(tr("Failed to initialize preferences. Failed to reset preference file to default: {0}", getPreferenceFile()));
699            }
700        }
701        File def = getDefaultsCacheFile();
702        if (def.exists()) {
703            try {
704                loadDefaults();
705            } catch (IOException | XMLStreamException | SAXException e) {
706                Main.error(e);
707                Main.warn(tr("Failed to load defaults cache file: {0}", def));
708                defaultsMap.clear();
709                if (!def.delete()) {
710                    Main.warn(tr("Failed to delete faulty defaults cache file: {0}", def));
711                }
712            }
713        }
714    }
715
716    public final void resetToDefault() {
717        settingsMap.clear();
718    }
719
720    /**
721     * Convenience method for accessing colour preferences.
722     *
723     * @param colName name of the colour
724     * @param def default value
725     * @return a Color object for the configured colour, or the default value if none configured.
726     */
727    public synchronized Color getColor(String colName, Color def) {
728        return getColor(colName, null, def);
729    }
730
731    /* only for preferences */
732    public synchronized String getColorName(String o) {
733        try {
734            Matcher m = Pattern.compile("mappaint\\.(.+?)\\.(.+)").matcher(o);
735            if (m.matches()) {
736                return tr("Paint style {0}: {1}", tr(I18n.escape(m.group(1))), tr(I18n.escape(m.group(2))));
737            }
738        } catch (Exception e) {
739            Main.warn(e);
740        }
741        try {
742            Matcher m = Pattern.compile("layer (.+)").matcher(o);
743            if (m.matches()) {
744                return tr("Layer: {0}", tr(I18n.escape(m.group(1))));
745            }
746        } catch (Exception e) {
747            Main.warn(e);
748        }
749        return tr(I18n.escape(colornames.containsKey(o) ? colornames.get(o) : o));
750    }
751
752    /**
753     * Returns the color for the given key.
754     * @param key The color key
755     * @return the color
756     */
757    public Color getColor(ColorKey key) {
758        return getColor(key.getColorName(), key.getSpecialName(), key.getDefaultValue());
759    }
760
761    /**
762     * Convenience method for accessing colour preferences.
763     *
764     * @param colName name of the colour
765     * @param specName name of the special colour settings
766     * @param def default value
767     * @return a Color object for the configured colour, or the default value if none configured.
768     */
769    public synchronized Color getColor(String colName, String specName, Color def) {
770        String colKey = ColorProperty.getColorKey(colName);
771        if (!colKey.equals(colName)) {
772            colornames.put(colKey, colName);
773        }
774        String colStr = specName != null ? get("color."+specName) : "";
775        if (colStr.isEmpty()) {
776            colStr = get("color." + colKey, ColorHelper.color2html(def, true));
777        }
778        if (colStr != null && !colStr.isEmpty()) {
779            return ColorHelper.html2color(colStr);
780        } else {
781            return def;
782        }
783    }
784
785    public synchronized Color getDefaultColor(String colKey) {
786        StringSetting col = Utils.cast(defaultsMap.get("color."+colKey), StringSetting.class);
787        String colStr = col == null ? null : col.getValue();
788        return colStr == null || colStr.isEmpty() ? null : ColorHelper.html2color(colStr);
789    }
790
791    public synchronized boolean putColor(String colKey, Color val) {
792        return put("color."+colKey, val != null ? ColorHelper.color2html(val, true) : null);
793    }
794
795    public synchronized int getInteger(String key, int def) {
796        String v = get(key, Integer.toString(def));
797        if (v.isEmpty())
798            return def;
799
800        try {
801            return Integer.parseInt(v);
802        } catch (NumberFormatException e) {
803            // fall out
804            if (Main.isTraceEnabled()) {
805                Main.trace(e.getMessage());
806            }
807        }
808        return def;
809    }
810
811    public synchronized int getInteger(String key, String specName, int def) {
812        String v = get(key+'.'+specName);
813        if (v.isEmpty())
814            v = get(key, Integer.toString(def));
815        if (v.isEmpty())
816            return def;
817
818        try {
819            return Integer.parseInt(v);
820        } catch (NumberFormatException e) {
821            // fall out
822            if (Main.isTraceEnabled()) {
823                Main.trace(e.getMessage());
824            }
825        }
826        return def;
827    }
828
829    public synchronized long getLong(String key, long def) {
830        String v = get(key, Long.toString(def));
831        if (null == v)
832            return def;
833
834        try {
835            return Long.parseLong(v);
836        } catch (NumberFormatException e) {
837            // fall out
838            if (Main.isTraceEnabled()) {
839                Main.trace(e.getMessage());
840            }
841        }
842        return def;
843    }
844
845    public synchronized double getDouble(String key, double def) {
846        String v = get(key, Double.toString(def));
847        if (null == v)
848            return def;
849
850        try {
851            return Double.parseDouble(v);
852        } catch (NumberFormatException e) {
853            // fall out
854            if (Main.isTraceEnabled()) {
855                Main.trace(e.getMessage());
856            }
857        }
858        return def;
859    }
860
861    /**
862     * Get a list of values for a certain key
863     * @param key the identifier for the setting
864     * @param def the default value.
865     * @return the corresponding value if the property has been set before, {@code def} otherwise
866     */
867    public Collection<String> getCollection(String key, Collection<String> def) {
868        return getSetting(key, ListSetting.create(def), ListSetting.class).getValue();
869    }
870
871    /**
872     * Get a list of values for a certain key
873     * @param key the identifier for the setting
874     * @return the corresponding value if the property has been set before, an empty collection otherwise.
875     */
876    public Collection<String> getCollection(String key) {
877        Collection<String> val = getCollection(key, null);
878        return val == null ? Collections.<String>emptyList() : val;
879    }
880
881    public synchronized void removeFromCollection(String key, String value) {
882        List<String> a = new ArrayList<>(getCollection(key, Collections.<String>emptyList()));
883        a.remove(value);
884        putCollection(key, a);
885    }
886
887    /**
888     * Set a value for a certain setting. The changed setting is saved to the preference file immediately.
889     * Due to caching mechanisms on modern operating systems and hardware, this shouldn't be a performance problem.
890     * @param key the unique identifier for the setting
891     * @param setting the value of the setting. In case it is null, the key-value entry will be removed.
892     * @return {@code true}, if something has changed (i.e. value is different than before)
893     */
894    public boolean putSetting(final String key, Setting<?> setting) {
895        CheckParameterUtil.ensureParameterNotNull(key);
896        if (setting != null && setting.getValue() == null)
897            throw new IllegalArgumentException("setting argument must not have null value");
898        Setting<?> settingOld;
899        Setting<?> settingCopy = null;
900        synchronized (this) {
901            if (setting == null) {
902                settingOld = settingsMap.remove(key);
903                if (settingOld == null)
904                    return false;
905            } else {
906                settingOld = settingsMap.get(key);
907                if (setting.equals(settingOld))
908                    return false;
909                if (settingOld == null && setting.equals(defaultsMap.get(key)))
910                    return false;
911                settingCopy = setting.copy();
912                settingsMap.put(key, settingCopy);
913            }
914            if (saveOnPut) {
915                try {
916                    save();
917                } catch (IOException e) {
918                    Main.warn(tr("Failed to persist preferences to ''{0}''", getPreferenceFile().getAbsoluteFile()));
919                }
920            }
921        }
922        // Call outside of synchronized section in case some listener wait for other thread that wait for preference lock
923        firePreferenceChanged(key, settingOld, settingCopy);
924        return true;
925    }
926
927    public synchronized Setting<?> getSetting(String key, Setting<?> def) {
928        return getSetting(key, def, Setting.class);
929    }
930
931    /**
932     * Get settings value for a certain key and provide default a value.
933     * @param <T> the setting type
934     * @param key the identifier for the setting
935     * @param def the default value. For each call of getSetting() with a given key, the default value must be the same.
936     * <code>def</code> must not be null, but the value of <code>def</code> can be null.
937     * @param klass the setting type (same as T)
938     * @return the corresponding value if the property has been set before, {@code def} otherwise
939     */
940    @SuppressWarnings("unchecked")
941    public synchronized <T extends Setting<?>> T getSetting(String key, T def, Class<T> klass) {
942        CheckParameterUtil.ensureParameterNotNull(key);
943        CheckParameterUtil.ensureParameterNotNull(def);
944        Setting<?> oldDef = defaultsMap.get(key);
945        if (oldDef != null && oldDef.isNew() && oldDef.getValue() != null && def.getValue() != null && !def.equals(oldDef)) {
946            Main.info("Defaults for " + key + " differ: " + def + " != " + defaultsMap.get(key));
947        }
948        if (def.getValue() != null || oldDef == null) {
949            Setting<?> defCopy = def.copy();
950            defCopy.setTime(System.currentTimeMillis() / 1000);
951            defCopy.setNew(true);
952            defaultsMap.put(key, defCopy);
953        }
954        Setting<?> prop = settingsMap.get(key);
955        if (klass.isInstance(prop)) {
956            return (T) prop;
957        } else {
958            return def;
959        }
960    }
961
962    /**
963     * Put a collection.
964     * @param key key
965     * @param value value
966     * @return {@code true}, if something has changed (i.e. value is different than before)
967     */
968    public boolean putCollection(String key, Collection<String> value) {
969        return putSetting(key, value == null ? null : ListSetting.create(value));
970    }
971
972    /**
973     * Saves at most {@code maxsize} items of collection {@code val}.
974     * @param key key
975     * @param maxsize max number of items to save
976     * @param val value
977     * @return {@code true}, if something has changed (i.e. value is different than before)
978     */
979    public boolean putCollectionBounded(String key, int maxsize, Collection<String> val) {
980        Collection<String> newCollection = new ArrayList<>(Math.min(maxsize, val.size()));
981        for (String i : val) {
982            if (newCollection.size() >= maxsize) {
983                break;
984            }
985            newCollection.add(i);
986        }
987        return putCollection(key, newCollection);
988    }
989
990    /**
991     * Used to read a 2-dimensional array of strings from the preference file.
992     * If not a single entry could be found, <code>def</code> is returned.
993     * @param key preference key
994     * @param def default array value
995     * @return array value
996     */
997    @SuppressWarnings({ "unchecked", "rawtypes" })
998    public synchronized Collection<Collection<String>> getArray(String key, Collection<Collection<String>> def) {
999        ListListSetting val = getSetting(key, ListListSetting.create(def), ListListSetting.class);
1000        return (Collection) val.getValue();
1001    }
1002
1003    public Collection<Collection<String>> getArray(String key) {
1004        Collection<Collection<String>> res = getArray(key, null);
1005        return res == null ? Collections.<Collection<String>>emptyList() : res;
1006    }
1007
1008    /**
1009     * Put an array.
1010     * @param key key
1011     * @param value value
1012     * @return {@code true}, if something has changed (i.e. value is different than before)
1013     */
1014    public boolean putArray(String key, Collection<Collection<String>> value) {
1015        return putSetting(key, value == null ? null : ListListSetting.create(value));
1016    }
1017
1018    public Collection<Map<String, String>> getListOfStructs(String key, Collection<Map<String, String>> def) {
1019        return getSetting(key, new MapListSetting(def == null ? null : new ArrayList<>(def)), MapListSetting.class).getValue();
1020    }
1021
1022    public boolean putListOfStructs(String key, Collection<Map<String, String>> value) {
1023        return putSetting(key, value == null ? null : new MapListSetting(new ArrayList<>(value)));
1024    }
1025
1026    /**
1027     * Annotation used for converting objects to String Maps and vice versa.
1028     * Indicates that a certain field should be considered in the conversion process. Otherwise it is ignored.
1029     *
1030     * @see #serializeStruct(java.lang.Object, java.lang.Class)
1031     * @see #deserializeStruct(java.util.Map, java.lang.Class)
1032     */
1033    @Retention(RetentionPolicy.RUNTIME) // keep annotation at runtime
1034    public @interface pref { }
1035
1036    /**
1037     * Annotation used for converting objects to String Maps.
1038     * Indicates that a certain field should be written to the map, even if the value is the same as the default value.
1039     *
1040     * @see #serializeStruct(java.lang.Object, java.lang.Class)
1041     */
1042    @Retention(RetentionPolicy.RUNTIME) // keep annotation at runtime
1043    public @interface writeExplicitly { }
1044
1045    /**
1046     * Get a list of hashes which are represented by a struct-like class.
1047     * Possible properties are given by fields of the class klass that have the @pref annotation.
1048     * Default constructor is used to initialize the struct objects, properties then override some of these default values.
1049     * @param <T> klass type
1050     * @param key main preference key
1051     * @param klass The struct class
1052     * @return a list of objects of type T or an empty list if nothing was found
1053     */
1054    public <T> List<T> getListOfStructs(String key, Class<T> klass) {
1055        List<T> r = getListOfStructs(key, null, klass);
1056        if (r == null)
1057            return Collections.emptyList();
1058        else
1059            return r;
1060    }
1061
1062    /**
1063     * same as above, but returns def if nothing was found
1064     * @param <T> klass type
1065     * @param key main preference key
1066     * @param def default value
1067     * @param klass The struct class
1068     * @return a list of objects of type T or {@code def} if nothing was found
1069     */
1070    public <T> List<T> getListOfStructs(String key, Collection<T> def, Class<T> klass) {
1071        Collection<Map<String, String>> prop =
1072            getListOfStructs(key, def == null ? null : serializeListOfStructs(def, klass));
1073        if (prop == null)
1074            return def == null ? null : new ArrayList<>(def);
1075        List<T> lst = new ArrayList<>();
1076        for (Map<String, String> entries : prop) {
1077            T struct = deserializeStruct(entries, klass);
1078            lst.add(struct);
1079        }
1080        return lst;
1081    }
1082
1083    /**
1084     * Convenience method that saves a MapListSetting which is provided as a collection of objects.
1085     *
1086     * Each object is converted to a <code>Map&lt;String, String&gt;</code> using the fields with {@link pref} annotation.
1087     * The field name is the key and the value will be converted to a string.
1088     *
1089     * Considers only fields that have the @pref annotation.
1090     * In addition it does not write fields with null values. (Thus they are cleared)
1091     * Default values are given by the field values after default constructor has been called.
1092     * Fields equal to the default value are not written unless the field has the @writeExplicitly annotation.
1093     * @param <T> the class,
1094     * @param key main preference key
1095     * @param val the list that is supposed to be saved
1096     * @param klass The struct class
1097     * @return true if something has changed
1098     */
1099    public <T> boolean putListOfStructs(String key, Collection<T> val, Class<T> klass) {
1100        return putListOfStructs(key, serializeListOfStructs(val, klass));
1101    }
1102
1103    private static <T> Collection<Map<String, String>> serializeListOfStructs(Collection<T> l, Class<T> klass) {
1104        if (l == null)
1105            return null;
1106        Collection<Map<String, String>> vals = new ArrayList<>();
1107        for (T struct : l) {
1108            if (struct == null) {
1109                continue;
1110            }
1111            vals.add(serializeStruct(struct, klass));
1112        }
1113        return vals;
1114    }
1115
1116    @SuppressWarnings("rawtypes")
1117    private static String mapToJson(Map map) {
1118        StringWriter stringWriter = new StringWriter();
1119        try (JsonWriter writer = Json.createWriter(stringWriter)) {
1120            JsonObjectBuilder object = Json.createObjectBuilder();
1121            for (Object o: map.entrySet()) {
1122                Entry e = (Entry) o;
1123                Object evalue = e.getValue();
1124                object.add(e.getKey().toString(), evalue.toString());
1125            }
1126            writer.writeObject(object.build());
1127        }
1128        return stringWriter.toString();
1129    }
1130
1131    @SuppressWarnings({ "rawtypes", "unchecked" })
1132    private static Map mapFromJson(String s) {
1133        Map ret = null;
1134        try (JsonReader reader = Json.createReader(new StringReader(s))) {
1135            JsonObject object = reader.readObject();
1136            ret = new HashMap(object.size());
1137            for (Entry<String, JsonValue> e: object.entrySet()) {
1138                JsonValue value = e.getValue();
1139                if (value instanceof JsonString) {
1140                    // in some cases, when JsonValue.toString() is called, then additional quotation marks are left in value
1141                    ret.put(e.getKey(), ((JsonString) value).getString());
1142                } else {
1143                    ret.put(e.getKey(), e.getValue().toString());
1144                }
1145            }
1146        }
1147        return ret;
1148    }
1149
1150    @SuppressWarnings("rawtypes")
1151    private static String multiMapToJson(MultiMap map) {
1152        StringWriter stringWriter = new StringWriter();
1153        try (JsonWriter writer = Json.createWriter(stringWriter)) {
1154            JsonObjectBuilder object = Json.createObjectBuilder();
1155            for (Object o: map.entrySet()) {
1156                Entry e = (Entry) o;
1157                Set evalue = (Set) e.getValue();
1158                JsonArrayBuilder a = Json.createArrayBuilder();
1159                for (Object evo: evalue) {
1160                    a.add(evo.toString());
1161                }
1162                object.add(e.getKey().toString(), a.build());
1163            }
1164            writer.writeObject(object.build());
1165        }
1166        return stringWriter.toString();
1167    }
1168
1169    @SuppressWarnings({ "rawtypes", "unchecked" })
1170    private static MultiMap multiMapFromJson(String s) {
1171        MultiMap ret = null;
1172        try (JsonReader reader = Json.createReader(new StringReader(s))) {
1173            JsonObject object = reader.readObject();
1174            ret = new MultiMap(object.size());
1175            for (Entry<String, JsonValue> e: object.entrySet()) {
1176                JsonValue value = e.getValue();
1177                if (value instanceof JsonArray) {
1178                    for (JsonString js: ((JsonArray) value).getValuesAs(JsonString.class)) {
1179                        ret.put(e.getKey(), js.getString());
1180                    }
1181                } else if (value instanceof JsonString) {
1182                    // in some cases, when JsonValue.toString() is called, then additional quotation marks are left in value
1183                    ret.put(e.getKey(), ((JsonString) value).getString());
1184                } else {
1185                    ret.put(e.getKey(), e.getValue().toString());
1186                }
1187            }
1188        }
1189        return ret;
1190    }
1191
1192    /**
1193     * Convert an object to a String Map, by using field names and values as map key and value.
1194     *
1195     * The field value is converted to a String.
1196     *
1197     * Only fields with annotation {@link pref} are taken into account.
1198     *
1199     * Fields will not be written to the map if the value is null or unchanged
1200     * (compared to an object created with the no-arg-constructor).
1201     * The {@link writeExplicitly} annotation overrides this behavior, i.e. the default value will also be written.
1202     *
1203     * @param <T> the class of the object <code>struct</code>
1204     * @param struct the object to be converted
1205     * @param klass the class T
1206     * @return the resulting map (same data content as <code>struct</code>)
1207     */
1208    public static <T> Map<String, String> serializeStruct(T struct, Class<T> klass) {
1209        T structPrototype;
1210        try {
1211            structPrototype = klass.newInstance();
1212        } catch (InstantiationException | IllegalAccessException ex) {
1213            throw new RuntimeException(ex);
1214        }
1215
1216        Map<String, String> hash = new LinkedHashMap<>();
1217        for (Field f : klass.getDeclaredFields()) {
1218            if (f.getAnnotation(pref.class) == null) {
1219                continue;
1220            }
1221            f.setAccessible(true);
1222            try {
1223                Object fieldValue = f.get(struct);
1224                Object defaultFieldValue = f.get(structPrototype);
1225                if (fieldValue != null) {
1226                    if (f.getAnnotation(writeExplicitly.class) != null || !Objects.equals(fieldValue, defaultFieldValue)) {
1227                        String key = f.getName().replace('_', '-');
1228                        if (fieldValue instanceof Map) {
1229                            hash.put(key, mapToJson((Map) fieldValue));
1230                        } else if (fieldValue instanceof MultiMap) {
1231                            hash.put(key, multiMapToJson((MultiMap) fieldValue));
1232                        } else {
1233                            hash.put(key, fieldValue.toString());
1234                        }
1235                    }
1236                }
1237            } catch (IllegalArgumentException | IllegalAccessException ex) {
1238                throw new RuntimeException(ex);
1239            }
1240        }
1241        return hash;
1242    }
1243
1244    /**
1245     * Converts a String-Map to an object of a certain class, by comparing map keys to field names of the class and assigning
1246     * map values to the corresponding fields.
1247     *
1248     * The map value (a String) is converted to the field type. Supported types are: boolean, Boolean, int, Integer, double,
1249     * Double, String, Map&lt;String, String&gt; and Map&lt;String, List&lt;String&gt;&gt;.
1250     *
1251     * Only fields with annotation {@link pref} are taken into account.
1252     * @param <T> the class
1253     * @param hash the string map with initial values
1254     * @param klass the class T
1255     * @return an object of class T, initialized as described above
1256     */
1257    public static <T> T deserializeStruct(Map<String, String> hash, Class<T> klass) {
1258        T struct = null;
1259        try {
1260            struct = klass.newInstance();
1261        } catch (InstantiationException | IllegalAccessException ex) {
1262            throw new RuntimeException(ex);
1263        }
1264        for (Entry<String, String> key_value : hash.entrySet()) {
1265            Object value = null;
1266            Field f;
1267            try {
1268                f = klass.getDeclaredField(key_value.getKey().replace('-', '_'));
1269            } catch (NoSuchFieldException ex) {
1270                continue;
1271            } catch (SecurityException ex) {
1272                throw new RuntimeException(ex);
1273            }
1274            if (f.getAnnotation(pref.class) == null) {
1275                continue;
1276            }
1277            f.setAccessible(true);
1278            if (f.getType() == Boolean.class || f.getType() == boolean.class) {
1279                value = Boolean.valueOf(key_value.getValue());
1280            } else if (f.getType() == Integer.class || f.getType() == int.class) {
1281                try {
1282                    value = Integer.valueOf(key_value.getValue());
1283                } catch (NumberFormatException nfe) {
1284                    continue;
1285                }
1286            } else if (f.getType() == Double.class || f.getType() == double.class) {
1287                try {
1288                    value = Double.valueOf(key_value.getValue());
1289                } catch (NumberFormatException nfe) {
1290                    continue;
1291                }
1292            } else  if (f.getType() == String.class) {
1293                value = key_value.getValue();
1294            } else if (f.getType().isAssignableFrom(Map.class)) {
1295                value = mapFromJson(key_value.getValue());
1296            } else if (f.getType().isAssignableFrom(MultiMap.class)) {
1297                value = multiMapFromJson(key_value.getValue());
1298            } else
1299                throw new RuntimeException("unsupported preference primitive type");
1300
1301            try {
1302                f.set(struct, value);
1303            } catch (IllegalArgumentException ex) {
1304                throw new AssertionError(ex);
1305            } catch (IllegalAccessException ex) {
1306                throw new RuntimeException(ex);
1307            }
1308        }
1309        return struct;
1310    }
1311
1312    public Map<String, Setting<?>> getAllSettings() {
1313        return new TreeMap<>(settingsMap);
1314    }
1315
1316    public Map<String, Setting<?>> getAllDefaults() {
1317        return new TreeMap<>(defaultsMap);
1318    }
1319
1320    /**
1321     * Updates system properties with the current values in the preferences.
1322     *
1323     */
1324    public void updateSystemProperties() {
1325        if ("true".equals(get("prefer.ipv6", "auto")) && !"true".equals(Utils.updateSystemProperty("java.net.preferIPv6Addresses", "true"))) {
1326            // never set this to false, only true!
1327            Main.info(tr("Try enabling IPv6 network, prefering IPv6 over IPv4 (only works on early startup)."));
1328        }
1329        Utils.updateSystemProperty("http.agent", Version.getInstance().getAgentString());
1330        Utils.updateSystemProperty("user.language", get("language"));
1331        // Workaround to fix a Java bug. This ugly hack comes from Sun bug database: https://bugs.openjdk.java.net/browse/JDK-6292739
1332        // Force AWT toolkit to update its internal preferences (fix #6345).
1333        if (!GraphicsEnvironment.isHeadless()) {
1334            try {
1335                Field field = Toolkit.class.getDeclaredField("resources");
1336                field.setAccessible(true);
1337                field.set(null, ResourceBundle.getBundle("sun.awt.resources.awt"));
1338            } catch (Exception | InternalError e) {
1339                // Ignore all exceptions, including internal error raised by Java 9 Jigsaw EA:
1340                // java.lang.InternalError: legacy getBundle can't be used to find sun.awt.resources.awt in module java.desktop
1341                // InternalError catch to remove when https://bugs.openjdk.java.net/browse/JDK-8136804 is resolved
1342                if (Main.isTraceEnabled()) {
1343                    Main.trace(e.getMessage());
1344                }
1345            }
1346        }
1347        // Possibility to disable SNI (not by default) in case of misconfigured https servers
1348        // See #9875 + http://stackoverflow.com/a/14884941/2257172
1349        // then https://josm.openstreetmap.de/ticket/12152#comment:5 for details
1350        if (getBoolean("jdk.tls.disableSNIExtension", false)) {
1351            Utils.updateSystemProperty("jsse.enableSNIExtension", "false");
1352        }
1353        // Workaround to fix another Java bug - The bug seems to have been fixed in Java 8, to remove during transition
1354        // Force Java 7 to use old sorting algorithm of Arrays.sort (fix #8712).
1355        // See Oracle bug database: https://bugs.openjdk.java.net/browse/JDK-7075600
1356        // and https://bugs.openjdk.java.net/browse/JDK-6923200
1357        if (getBoolean("jdk.Arrays.useLegacyMergeSort", !Version.getInstance().isLocalBuild())) {
1358            Utils.updateSystemProperty("java.util.Arrays.useLegacyMergeSort", "true");
1359        }
1360    }
1361
1362    /**
1363     * Replies the collection of plugin site URLs from where plugin lists can be downloaded.
1364     * @return the collection of plugin site URLs
1365     * @see #getOnlinePluginSites
1366     */
1367    public Collection<String> getPluginSites() {
1368        return getCollection("pluginmanager.sites", Collections.singleton(Main.getJOSMWebsite()+"/pluginicons%<?plugins=>"));
1369    }
1370
1371    /**
1372     * Returns the list of plugin sites available according to offline mode settings.
1373     * @return the list of available plugin sites
1374     * @since 8471
1375     */
1376    public Collection<String> getOnlinePluginSites() {
1377        Collection<String> pluginSites = new ArrayList<>(getPluginSites());
1378        for (Iterator<String> it = pluginSites.iterator(); it.hasNext();) {
1379            try {
1380                OnlineResource.JOSM_WEBSITE.checkOfflineAccess(it.next(), Main.getJOSMWebsite());
1381            } catch (OfflineAccessException ex) {
1382                Main.warn(ex, false);
1383                it.remove();
1384            }
1385        }
1386        return pluginSites;
1387    }
1388
1389    /**
1390     * Sets the collection of plugin site URLs.
1391     *
1392     * @param sites the site URLs
1393     */
1394    public void setPluginSites(Collection<String> sites) {
1395        putCollection("pluginmanager.sites", sites);
1396    }
1397
1398    /**
1399     * Returns XML describing these preferences.
1400     * @param nopass if password must be excluded
1401     * @return XML
1402     */
1403    public String toXML(boolean nopass) {
1404        return toXML(settingsMap.entrySet(), nopass, false);
1405    }
1406
1407    /**
1408     * Returns XML describing the given preferences.
1409     * @param settings preferences settings
1410     * @param nopass if password must be excluded
1411     * @param defaults true, if default values are converted to XML, false for
1412     * regular preferences
1413     * @return XML
1414     */
1415    public String toXML(Collection<Entry<String, Setting<?>>> settings, boolean nopass, boolean defaults) {
1416        try (
1417            StringWriter sw = new StringWriter();
1418            PreferencesWriter prefWriter = new PreferencesWriter(new PrintWriter(sw), nopass, defaults);
1419        ) {
1420            prefWriter.write(settings);
1421            sw.flush();
1422            return sw.toString();
1423        } catch (IOException e) {
1424            Main.error(e);
1425            return null;
1426        }
1427    }
1428
1429    /**
1430     * Removes obsolete preference settings. If you throw out a once-used preference
1431     * setting, add it to the list here with an expiry date (written as comment). If you
1432     * see something with an expiry date in the past, remove it from the list.
1433     * @param loadedVersion JOSM version when the preferences file was written
1434     */
1435    private void removeObsolete(int loadedVersion) {
1436        /* drop in October 2016 */
1437        if (loadedVersion < 9715) {
1438            Setting<?> setting = settingsMap.get("imagery.entries");
1439            if (setting instanceof MapListSetting) {
1440                List<Map<String, String>> l = new LinkedList<>();
1441                boolean modified = false;
1442                for (Map<String, String> map: ((MapListSetting) setting).getValue()) {
1443                    Map<String, String> newMap = new HashMap<>();
1444                    for (Entry<String, String> entry: map.entrySet()) {
1445                        String value = entry.getValue();
1446                        if ("noTileHeaders".equals(entry.getKey())) {
1447                            value = value.replaceFirst("\":(\".*\")\\}", "\":[$1]}");
1448                            if (!value.equals(entry.getValue())) {
1449                                modified = true;
1450                            }
1451                        }
1452                        newMap.put(entry.getKey(), value);
1453                    }
1454                    l.add(newMap);
1455                }
1456                if (modified) {
1457                    putListOfStructs("imagery.entries", l);
1458                }
1459            }
1460        }
1461        // drop in November 2016
1462        if (loadedVersion < 9965) {
1463            Setting<?> setting = settingsMap.get("mappaint.style.entries");
1464            if (setting instanceof MapListSetting) {
1465                List<Map<String, String>> l = new LinkedList<>();
1466                boolean modified = false;
1467                for (Map<String, String> map: ((MapListSetting) setting).getValue()) {
1468                    String url = map.get("url");
1469                    if (url != null && url.contains("josm.openstreetmap.de/josmfile?page=Styles/LegacyStandard")) {
1470                        modified = true;
1471                    } else {
1472                        l.add(map);
1473                    }
1474                }
1475                if (modified) {
1476                    putListOfStructs("mappaint.style.entries", l);
1477                }
1478            }
1479        }
1480
1481        for (String key : OBSOLETE_PREF_KEYS) {
1482            if (settingsMap.containsKey(key)) {
1483                settingsMap.remove(key);
1484                Main.info(tr("Preference setting {0} has been removed since it is no longer used.", key));
1485            }
1486        }
1487    }
1488
1489    /**
1490     * Enables or not the preferences file auto-save mechanism (save each time a setting is changed).
1491     * This behaviour is enabled by default.
1492     * @param enable if {@code true}, makes JOSM save preferences file each time a setting is changed
1493     * @since 7085
1494     */
1495    public final void enableSaveOnPut(boolean enable) {
1496        synchronized (this) {
1497            saveOnPut = enable;
1498        }
1499    }
1500}