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