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;
006import static org.openstreetmap.josm.tools.Utils.getSystemEnv;
007import static org.openstreetmap.josm.tools.Utils.getSystemProperty;
008
009import java.awt.GraphicsEnvironment;
010import java.io.File;
011import java.io.IOException;
012import java.io.PrintWriter;
013import java.io.Reader;
014import java.io.StringWriter;
015import java.nio.charset.StandardCharsets;
016import java.nio.file.InvalidPathException;
017import java.util.ArrayList;
018import java.util.Arrays;
019import java.util.Collection;
020import java.util.Collections;
021import java.util.HashMap;
022import java.util.HashSet;
023import java.util.Iterator;
024import java.util.List;
025import java.util.Map;
026import java.util.Map.Entry;
027import java.util.Optional;
028import java.util.Set;
029import java.util.SortedMap;
030import java.util.TreeMap;
031import java.util.concurrent.TimeUnit;
032import java.util.function.Predicate;
033import java.util.stream.Collectors;
034import java.util.stream.Stream;
035
036import javax.swing.JOptionPane;
037import javax.xml.stream.XMLStreamException;
038
039import org.openstreetmap.josm.data.preferences.ColorInfo;
040import org.openstreetmap.josm.data.preferences.JosmBaseDirectories;
041import org.openstreetmap.josm.data.preferences.NamedColorProperty;
042import org.openstreetmap.josm.data.preferences.PreferencesReader;
043import org.openstreetmap.josm.data.preferences.PreferencesWriter;
044import org.openstreetmap.josm.gui.MainApplication;
045import org.openstreetmap.josm.io.OfflineAccessException;
046import org.openstreetmap.josm.io.OnlineResource;
047import org.openstreetmap.josm.spi.preferences.AbstractPreferences;
048import org.openstreetmap.josm.spi.preferences.Config;
049import org.openstreetmap.josm.spi.preferences.DefaultPreferenceChangeEvent;
050import org.openstreetmap.josm.spi.preferences.IBaseDirectories;
051import org.openstreetmap.josm.spi.preferences.ListSetting;
052import org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent;
053import org.openstreetmap.josm.spi.preferences.Setting;
054import org.openstreetmap.josm.tools.CheckParameterUtil;
055import org.openstreetmap.josm.tools.ListenerList;
056import org.openstreetmap.josm.tools.Logging;
057import org.openstreetmap.josm.tools.PlatformManager;
058import org.openstreetmap.josm.tools.ReflectionUtils;
059import org.openstreetmap.josm.tools.Utils;
060import org.xml.sax.SAXException;
061
062/**
063 * This class holds all preferences for JOSM.
064 *
065 * Other classes can register their beloved properties here. All properties will be
066 * saved upon set-access.
067 *
068 * Each property is a key=setting pair, where key is a String and setting can be one of
069 * 4 types:
070 *     string, list, list of lists and list of maps.
071 * In addition, each key has a unique default value that is set when the value is first
072 * accessed using one of the get...() methods. You can use the same preference
073 * key in different parts of the code, but the default value must be the same
074 * everywhere. A default value of null means, the setting has been requested, but
075 * no default value was set. This is used in advanced preferences to present a list
076 * off all possible settings.
077 *
078 * At the moment, you cannot put the empty string for string properties.
079 * put(key, "") means, the property is removed.
080 *
081 * @author imi
082 * @since 74
083 */
084public class Preferences extends AbstractPreferences {
085
086    /** remove if key equals */
087    private static final String[] OBSOLETE_PREF_KEYS = {
088        "remotecontrol.https.enabled", /* remove entry after Dec. 2019 */
089        "remotecontrol.https.port", /* remove entry after Dec. 2019 */
090    };
091
092    /** remove if key starts with */
093    private static final String[] OBSOLETE_PREF_KEYS_START = {
094            //only remove layer specific prefs
095            "draw.rawgps.layer.wpt.",
096            "draw.rawgps.layer.audiowpt.",
097            "draw.rawgps.lines.force.",
098            "draw.rawgps.lines.alpha-blend.",
099            "draw.rawgps.lines.",
100            "markers.show ", //uses space as separator
101            "marker.makeautomarker.",
102            "clr.layer.",
103
104            //remove both layer specific and global prefs
105            "draw.rawgps.colors",
106            "draw.rawgps.direction",
107            "draw.rawgps.alternatedirection",
108            "draw.rawgps.linewidth",
109            "draw.rawgps.max-line-length.local",
110            "draw.rawgps.max-line-length",
111            "draw.rawgps.large",
112            "draw.rawgps.large.size",
113            "draw.rawgps.hdopcircle",
114            "draw.rawgps.min-arrow-distance",
115            "draw.rawgps.colorTracksTune",
116            "draw.rawgps.colors.dynamic",
117            "draw.rawgps.lines.local",
118            "draw.rawgps.heatmap"
119    };
120
121    /** keep subkey even if it starts with any of {@link #OBSOLETE_PREF_KEYS_START} */
122    private static final List<String> KEEP_PREF_KEYS = Arrays.asList(
123            "draw.rawgps.lines.alpha-blend",
124            "draw.rawgps.lines.arrows",
125            "draw.rawgps.lines.arrows.fast",
126            "draw.rawgps.lines.arrows.min-distance",
127            "draw.rawgps.lines.force",
128            "draw.rawgps.lines.max-length",
129            "draw.rawgps.lines.max-length.local",
130            "draw.rawgps.lines.width"
131    );
132
133    /** rename keys that equal */
134    private static final Map<String, String> UPDATE_PREF_KEYS = getUpdatePrefKeys();
135
136    private static Map<String, String> getUpdatePrefKeys() {
137        HashMap<String, String> m = new HashMap<>();
138        m.put("draw.rawgps.direction", "draw.rawgps.lines.arrows");
139        m.put("draw.rawgps.alternatedirection", "draw.rawgps.lines.arrows.fast");
140        m.put("draw.rawgps.min-arrow-distance", "draw.rawgps.lines.arrows.min-distance");
141        m.put("draw.rawgps.linewidth", "draw.rawgps.lines.width");
142        m.put("draw.rawgps.max-line-length.local", "draw.rawgps.lines.max-length.local");
143        m.put("draw.rawgps.max-line-length", "draw.rawgps.lines.max-length");
144        m.put("draw.rawgps.large", "draw.rawgps.points.large");
145        m.put("draw.rawgps.large.alpha", "draw.rawgps.points.large.alpha");
146        m.put("draw.rawgps.large.size", "draw.rawgps.points.large.size");
147        m.put("draw.rawgps.hdopcircle", "draw.rawgps.points.hdopcircle");
148        m.put("draw.rawgps.layer.wpt.pattern", "draw.rawgps.markers.pattern");
149        m.put("draw.rawgps.layer.audiowpt.pattern", "draw.rawgps.markers.audio.pattern");
150        m.put("draw.rawgps.colors", "draw.rawgps.colormode");
151        m.put("draw.rawgps.colorTracksTune", "draw.rawgps.colormode.velocity.tune");
152        m.put("draw.rawgps.colors.dynamic", "draw.rawgps.colormode.dynamic-range");
153        m.put("draw.rawgps.heatmap.line-extra", "draw.rawgps.colormode.heatmap.line-extra");
154        m.put("draw.rawgps.heatmap.colormap", "draw.rawgps.colormode.heatmap.colormap");
155        m.put("draw.rawgps.heatmap.use-points", "draw.rawgps.colormode.heatmap.use-points");
156        m.put("draw.rawgps.heatmap.gain", "draw.rawgps.colormode.heatmap.gain");
157        m.put("draw.rawgps.heatmap.lower-limit", "draw.rawgps.colormode.heatmap.lower-limit");
158        m.put("draw.rawgps.date-coloring-min-dt", "draw.rawgps.colormode.time.min-distance");
159        return Collections.unmodifiableMap(m);
160    }
161
162    private static final long MAX_AGE_DEFAULT_PREFERENCES = TimeUnit.DAYS.toSeconds(50);
163
164    private final IBaseDirectories dirs;
165    boolean modifiedDefault;
166
167    /**
168     * Determines if preferences file is saved each time a property is changed.
169     */
170    private boolean saveOnPut = true;
171
172    /**
173     * Maps the setting name to the current value of the setting.
174     * The map must not contain null as key or value. The mapped setting objects
175     * must not have a null value.
176     */
177    protected final SortedMap<String, Setting<?>> settingsMap = new TreeMap<>();
178
179    /**
180     * Maps the setting name to the default value of the setting.
181     * The map must not contain null as key or value. The value of the mapped
182     * setting objects can be null.
183     */
184    protected final SortedMap<String, Setting<?>> defaultsMap = new TreeMap<>();
185
186    private final Predicate<Entry<String, Setting<?>>> NO_DEFAULT_SETTINGS_ENTRY =
187            e -> !e.getValue().equals(defaultsMap.get(e.getKey()));
188
189    /**
190     * Indicates whether {@link #init(boolean)} completed successfully.
191     * Used to decide whether to write backup preference file in {@link #save()}
192     */
193    protected boolean initSuccessful;
194
195    private final ListenerList<org.openstreetmap.josm.spi.preferences.PreferenceChangedListener> listeners = ListenerList.create();
196
197    private final HashMap<String, ListenerList<org.openstreetmap.josm.spi.preferences.PreferenceChangedListener>> keyListeners = new HashMap<>();
198
199    private static final Preferences defaultInstance = new Preferences(JosmBaseDirectories.getInstance());
200
201    /**
202     * Preferences classes calling directly the method {@link #putSetting(String, Setting)}.
203     * This collection allows us to exclude them when searching the business class who set a preference.
204     * The found class is used as event source when notifying event listeners.
205     */
206    private static final Collection<Class<?>> preferencesClasses = Arrays.asList(
207            Preferences.class, PreferencesUtils.class, AbstractPreferences.class);
208
209    /**
210     * Constructs a new {@code Preferences}.
211     */
212    public Preferences() {
213        this.dirs = Config.getDirs();
214    }
215
216    /**
217     * Constructs a new {@code Preferences}.
218     *
219     * @param dirs the directories to use for saving the preferences
220     */
221    public Preferences(IBaseDirectories dirs) {
222        this.dirs = dirs;
223    }
224
225    /**
226     * Constructs a new {@code Preferences} from an existing instance.
227     * @param pref existing preferences to copy
228     * @since 12634
229     */
230    public Preferences(Preferences pref) {
231        this(pref.dirs);
232        settingsMap.putAll(pref.settingsMap);
233        defaultsMap.putAll(pref.defaultsMap);
234    }
235
236    /**
237     * Returns the main (default) preferences instance.
238     * @return the main (default) preferences instance
239     * @since 14149
240     */
241    public static Preferences main() {
242        return defaultInstance;
243    }
244
245    /**
246     * Adds a new preferences listener.
247     * @param listener The listener to add
248     * @since 12881
249     */
250    @Override
251    public void addPreferenceChangeListener(org.openstreetmap.josm.spi.preferences.PreferenceChangedListener listener) {
252        if (listener != null) {
253            listeners.addListener(listener);
254        }
255    }
256
257    /**
258     * Removes a preferences listener.
259     * @param listener The listener to remove
260     * @since 12881
261     */
262    @Override
263    public void removePreferenceChangeListener(org.openstreetmap.josm.spi.preferences.PreferenceChangedListener listener) {
264        listeners.removeListener(listener);
265    }
266
267    /**
268     * Adds a listener that only listens to changes in one preference
269     * @param key The preference key to listen to
270     * @param listener The listener to add.
271     * @since 12881
272     */
273    @Override
274    public void addKeyPreferenceChangeListener(String key, org.openstreetmap.josm.spi.preferences.PreferenceChangedListener listener) {
275        listenersForKey(key).addListener(listener);
276    }
277
278    /**
279     * Adds a weak listener that only listens to changes in one preference
280     * @param key The preference key to listen to
281     * @param listener The listener to add.
282     * @since 10824
283     */
284    public void addWeakKeyPreferenceChangeListener(String key, org.openstreetmap.josm.spi.preferences.PreferenceChangedListener listener) {
285        listenersForKey(key).addWeakListener(listener);
286    }
287
288    private ListenerList<org.openstreetmap.josm.spi.preferences.PreferenceChangedListener> listenersForKey(String key) {
289        return keyListeners.computeIfAbsent(key, k -> ListenerList.create());
290    }
291
292    /**
293     * Removes a listener that only listens to changes in one preference
294     * @param key The preference key to listen to
295     * @param listener The listener to add.
296     * @since 12881
297     */
298    @Override
299    public void removeKeyPreferenceChangeListener(String key, org.openstreetmap.josm.spi.preferences.PreferenceChangedListener listener) {
300        Optional.ofNullable(keyListeners.get(key)).orElseThrow(
301                () -> new IllegalArgumentException("There are no listeners registered for " + key))
302        .removeListener(listener);
303    }
304
305    protected void firePreferenceChanged(String key, Setting<?> oldValue, Setting<?> newValue) {
306        final Class<?> source = ReflectionUtils.findCallerClass(preferencesClasses);
307        final PreferenceChangeEvent evt =
308                new DefaultPreferenceChangeEvent(source != null ? source : getClass(), key, oldValue, newValue);
309        listeners.fireEvent(listener -> listener.preferenceChanged(evt));
310
311        ListenerList<org.openstreetmap.josm.spi.preferences.PreferenceChangedListener> forKey = keyListeners.get(key);
312        if (forKey != null) {
313            forKey.fireEvent(listener -> listener.preferenceChanged(evt));
314        }
315    }
316
317    /**
318     * Get the base name of the JOSM directories for preferences, cache and user data.
319     * Default value is "JOSM", unless overridden by system property "josm.dir.name".
320     * @return the base name of the JOSM directories for preferences, cache and user data
321     */
322    public static String getJOSMDirectoryBaseName() {
323        String name = getSystemProperty("josm.dir.name");
324        if (name != null)
325            return name;
326        else
327            return "JOSM";
328    }
329
330    /**
331     * Get the base directories associated with this preference instance.
332     * @return the base directories
333     */
334    public IBaseDirectories getDirs() {
335        return dirs;
336    }
337
338    /**
339     * Returns the user preferences file (preferences.xml).
340     * @return The user preferences file (preferences.xml)
341     */
342    public File getPreferenceFile() {
343        return new File(dirs.getPreferencesDirectory(false), "preferences.xml");
344    }
345
346    /**
347     * Returns the cache file for default preferences.
348     * @return the cache file for default preferences
349     */
350    public File getDefaultsCacheFile() {
351        return new File(dirs.getCacheDirectory(true), "default_preferences.xml");
352    }
353
354    /**
355     * Returns the user plugin directory.
356     * @return The user plugin directory
357     */
358    public File getPluginsDirectory() {
359        return new File(dirs.getUserDataDirectory(false), "plugins");
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 static Collection<String> getAllPossiblePreferenceDirs() {
376        Set<String> locations = new HashSet<>();
377        addPossibleResourceDir(locations, defaultInstance.dirs.getPreferencesDirectory(false).getPath());
378        addPossibleResourceDir(locations, defaultInstance.dirs.getUserDataDirectory(false).getPath());
379        addPossibleResourceDir(locations, getSystemEnv("JOSM_RESOURCES"));
380        addPossibleResourceDir(locations, getSystemProperty("josm.resources"));
381        locations.addAll(PlatformManager.getPlatform().getPossiblePreferenceDirs());
382        return locations;
383    }
384
385    /**
386     * Get all named colors, including customized and the default ones.
387     * @return a map of all named colors (maps preference key to {@link ColorInfo})
388     */
389    public synchronized Map<String, ColorInfo> getAllNamedColors() {
390        final Map<String, ColorInfo> all = new TreeMap<>();
391        for (final Entry<String, Setting<?>> e : settingsMap.entrySet()) {
392            if (!e.getKey().startsWith(NamedColorProperty.NAMED_COLOR_PREFIX))
393                continue;
394            Utils.instanceOfAndCast(e.getValue(), ListSetting.class)
395                    .map(ListSetting::getValue)
396                    .map(lst -> ColorInfo.fromPref(lst, false))
397                    .ifPresent(info -> all.put(e.getKey(), info));
398        }
399        for (final Entry<String, Setting<?>> e : defaultsMap.entrySet()) {
400            if (!e.getKey().startsWith(NamedColorProperty.NAMED_COLOR_PREFIX))
401                continue;
402            Utils.instanceOfAndCast(e.getValue(), ListSetting.class)
403                    .map(ListSetting::getValue)
404                    .map(lst -> ColorInfo.fromPref(lst, true))
405                    .ifPresent(infoDef -> {
406                        ColorInfo info = all.get(e.getKey());
407                        if (info == null) {
408                            all.put(e.getKey(), infoDef);
409                        } else {
410                            info.setDefaultValue(infoDef.getDefaultValue());
411                        }
412                    });
413        }
414        return all;
415    }
416
417    /**
418     * Called after every put. In case of a problem, do nothing but output the error in log.
419     * @throws IOException if any I/O error occurs
420     */
421    public synchronized void save() throws IOException {
422        save(getPreferenceFile(), settingsMap.entrySet().stream().filter(NO_DEFAULT_SETTINGS_ENTRY), false);
423    }
424
425    /**
426     * Stores the defaults to the defaults file
427     * @throws IOException If the file could not be saved
428     */
429    public synchronized void saveDefaults() throws IOException {
430        save(getDefaultsCacheFile(), defaultsMap.entrySet().stream(), true);
431    }
432
433    protected void save(File prefFile, Stream<Entry<String, Setting<?>>> settings, boolean defaults) throws IOException {
434        if (!defaults) {
435            /* currently unused, but may help to fix configuration issues in future */
436            putInt("josm.version", Version.getInstance().getVersion());
437        }
438
439        File backupFile = new File(prefFile + "_backup");
440
441        // Backup old preferences if there are old preferences
442        if (initSuccessful && prefFile.exists() && prefFile.length() > 0) {
443            Utils.copyFile(prefFile, backupFile);
444        }
445
446        try (PreferencesWriter writer = new PreferencesWriter(
447                new PrintWriter(new File(prefFile + "_tmp"), StandardCharsets.UTF_8.name()), false, defaults)) {
448            writer.write(settings);
449        } catch (SecurityException e) {
450            throw new IOException(e);
451        }
452
453        File tmpFile = new File(prefFile + "_tmp");
454        Utils.copyFile(tmpFile, prefFile);
455        Utils.deleteFile(tmpFile, marktr("Unable to delete temporary file {0}"));
456
457        setCorrectPermissions(prefFile);
458        setCorrectPermissions(backupFile);
459    }
460
461    private static void setCorrectPermissions(File file) {
462        if (!file.setReadable(false, false) && Logging.isTraceEnabled()) {
463            Logging.trace(tr("Unable to set file non-readable {0}", file.getAbsolutePath()));
464        }
465        if (!file.setWritable(false, false) && Logging.isTraceEnabled()) {
466            Logging.trace(tr("Unable to set file non-writable {0}", file.getAbsolutePath()));
467        }
468        if (!file.setExecutable(false, false) && Logging.isTraceEnabled()) {
469            Logging.trace(tr("Unable to set file non-executable {0}", file.getAbsolutePath()));
470        }
471        if (!file.setReadable(true, true) && Logging.isTraceEnabled()) {
472            Logging.trace(tr("Unable to set file readable {0}", file.getAbsolutePath()));
473        }
474        if (!file.setWritable(true, true) && Logging.isTraceEnabled()) {
475            Logging.trace(tr("Unable to set file writable {0}", file.getAbsolutePath()));
476        }
477    }
478
479    /**
480     * Loads preferences from settings file.
481     * @throws IOException if any I/O error occurs while reading the file
482     * @throws SAXException if the settings file does not contain valid XML
483     * @throws XMLStreamException if an XML error occurs while parsing the file (after validation)
484     */
485    protected void load() throws IOException, SAXException, XMLStreamException {
486        File pref = getPreferenceFile();
487        PreferencesReader.validateXML(pref);
488        PreferencesReader reader = new PreferencesReader(pref, false);
489        reader.parse();
490        settingsMap.clear();
491        settingsMap.putAll(reader.getSettings());
492        removeAndUpdateObsolete(reader.getVersion());
493    }
494
495    /**
496     * Loads default preferences from default settings cache file.
497     *
498     * Discards entries older than {@link #MAX_AGE_DEFAULT_PREFERENCES}.
499     *
500     * @throws IOException if any I/O error occurs while reading the file
501     * @throws SAXException if the settings file does not contain valid XML
502     * @throws XMLStreamException if an XML error occurs while parsing the file (after validation)
503     */
504    protected void loadDefaults() throws IOException, XMLStreamException, SAXException {
505        File def = getDefaultsCacheFile();
506        PreferencesReader.validateXML(def);
507        PreferencesReader reader = new PreferencesReader(def, true);
508        reader.parse();
509        defaultsMap.clear();
510        long minTime = System.currentTimeMillis() / 1000 - MAX_AGE_DEFAULT_PREFERENCES;
511        for (Entry<String, Setting<?>> e : reader.getSettings().entrySet()) {
512            if (e.getValue().getTime() >= minTime) {
513                defaultsMap.put(e.getKey(), e.getValue());
514            }
515        }
516    }
517
518    /**
519     * Loads preferences from XML reader.
520     * @param in XML reader
521     * @throws XMLStreamException if any XML stream error occurs
522     * @throws IOException if any I/O error occurs
523     */
524    public void fromXML(Reader in) throws XMLStreamException, IOException {
525        PreferencesReader reader = new PreferencesReader(in, false);
526        reader.parse();
527        settingsMap.clear();
528        settingsMap.putAll(reader.getSettings());
529    }
530
531    /**
532     * Initializes preferences.
533     * @param reset if {@code true}, current settings file is replaced by the default one
534     */
535    public void init(boolean reset) {
536        initSuccessful = false;
537        // get the preferences.
538        File prefDir = dirs.getPreferencesDirectory(false);
539        if (prefDir.exists()) {
540            if (!prefDir.isDirectory()) {
541                Logging.warn(tr("Failed to initialize preferences. Preference directory ''{0}'' is not a directory.",
542                        prefDir.getAbsoluteFile()));
543                if (!GraphicsEnvironment.isHeadless()) {
544                    JOptionPane.showMessageDialog(
545                            MainApplication.getMainFrame(),
546                            tr("<html>Failed to initialize preferences.<br>Preference directory ''{0}'' is not a directory.</html>",
547                                    prefDir.getAbsoluteFile()),
548                            tr("Error"),
549                            JOptionPane.ERROR_MESSAGE
550                    );
551                }
552                return;
553            }
554        } else {
555            if (!prefDir.mkdirs()) {
556                Logging.warn(tr("Failed to initialize preferences. Failed to create missing preference directory: {0}",
557                        prefDir.getAbsoluteFile()));
558                if (!GraphicsEnvironment.isHeadless()) {
559                    JOptionPane.showMessageDialog(
560                            MainApplication.getMainFrame(),
561                            tr("<html>Failed to initialize preferences.<br>Failed to create missing preference directory: {0}</html>",
562                                    prefDir.getAbsoluteFile()),
563                            tr("Error"),
564                            JOptionPane.ERROR_MESSAGE
565                    );
566                }
567                return;
568            }
569        }
570
571        File preferenceFile = getPreferenceFile();
572        try {
573            if (!preferenceFile.exists()) {
574                Logging.info(tr("Missing preference file ''{0}''. Creating a default preference file.", preferenceFile.getAbsoluteFile()));
575                resetToDefault();
576                save();
577            } else if (reset) {
578                File backupFile = new File(prefDir, "preferences.xml.bak");
579                PlatformManager.getPlatform().rename(preferenceFile, backupFile);
580                Logging.warn(tr("Replacing existing preference file ''{0}'' with default preference file.", preferenceFile.getAbsoluteFile()));
581                resetToDefault();
582                save();
583            }
584        } catch (IOException | InvalidPathException e) {
585            Logging.error(e);
586            if (!GraphicsEnvironment.isHeadless()) {
587                JOptionPane.showMessageDialog(
588                        MainApplication.getMainFrame(),
589                        tr("<html>Failed to initialize preferences.<br>Failed to reset preference file to default: {0}</html>",
590                                getPreferenceFile().getAbsoluteFile()),
591                        tr("Error"),
592                        JOptionPane.ERROR_MESSAGE
593                );
594            }
595            return;
596        }
597        File def = getDefaultsCacheFile();
598        if (def.exists()) {
599            try {
600                loadDefaults();
601            } catch (IOException | XMLStreamException | SAXException e) {
602                Logging.error(e);
603                Logging.warn(tr("Failed to load defaults cache file: {0}", def));
604                defaultsMap.clear();
605                if (!def.delete()) {
606                    Logging.warn(tr("Failed to delete faulty defaults cache file: {0}", def));
607                }
608            }
609        }
610        try {
611            load();
612            initSuccessful = true;
613        } catch (IOException | SAXException | XMLStreamException e) {
614            Logging.error(e);
615            File backupFile = new File(prefDir, "preferences.xml.bak");
616            if (!GraphicsEnvironment.isHeadless()) {
617                JOptionPane.showMessageDialog(
618                        MainApplication.getMainFrame(),
619                        tr("<html>Preferences file had errors.<br> Making backup of old one to <br>{0}<br> " +
620                                "and creating a new default preference file.</html>",
621                                backupFile.getAbsoluteFile()),
622                        tr("Error"),
623                        JOptionPane.ERROR_MESSAGE
624                );
625            }
626            PlatformManager.getPlatform().rename(preferenceFile, backupFile);
627            try {
628                resetToDefault();
629                save();
630            } catch (IOException e1) {
631                Logging.error(e1);
632                Logging.warn(tr("Failed to initialize preferences. Failed to reset preference file to default: {0}", getPreferenceFile()));
633            }
634        }
635    }
636
637    /**
638     * Resets the preferences to their initial state. This resets all values and file associations.
639     * The default values and listeners are not removed.
640     * <p>
641     * It is meant to be called before {@link #init(boolean)}
642     * @since 10876
643     */
644    public void resetToInitialState() {
645        resetToDefault();
646        saveOnPut = true;
647        initSuccessful = false;
648    }
649
650    /**
651     * Reset all values stored in this map to the default values. This clears the preferences.
652     */
653    public final void resetToDefault() {
654        settingsMap.clear();
655    }
656
657    /**
658     * Set a value for a certain setting. The changed setting is saved to the preference file immediately.
659     * Due to caching mechanisms on modern operating systems and hardware, this shouldn't be a performance problem.
660     * @param key the unique identifier for the setting
661     * @param setting the value of the setting. In case it is null, the key-value entry will be removed.
662     * @return {@code true}, if something has changed (i.e. value is different than before)
663     */
664    @Override
665    public boolean putSetting(final String key, Setting<?> setting) {
666        CheckParameterUtil.ensureParameterNotNull(key);
667        if (setting != null && setting.getValue() == null)
668            throw new IllegalArgumentException("setting argument must not have null value");
669        Setting<?> settingOld;
670        Setting<?> settingCopy = null;
671        synchronized (this) {
672            if (setting == null) {
673                settingOld = settingsMap.remove(key);
674                if (settingOld == null)
675                    return false;
676            } else {
677                settingOld = settingsMap.get(key);
678                if (setting.equals(settingOld))
679                    return false;
680                if (settingOld == null && setting.equals(defaultsMap.get(key)))
681                    return false;
682                settingCopy = setting.copy();
683                settingsMap.put(key, settingCopy);
684            }
685            if (saveOnPut) {
686                try {
687                    save();
688                } catch (IOException | InvalidPathException e) {
689                    File file = getPreferenceFile();
690                    try {
691                        file = file.getAbsoluteFile();
692                    } catch (SecurityException ex) {
693                        Logging.trace(ex);
694                    }
695                    Logging.log(Logging.LEVEL_WARN, tr("Failed to persist preferences to ''{0}''", file), e);
696                }
697            }
698        }
699        // Call outside of synchronized section in case some listener wait for other thread that wait for preference lock
700        firePreferenceChanged(key, settingOld, settingCopy);
701        return true;
702    }
703
704    /**
705     * Get a setting of any type
706     * @param key The key for the setting
707     * @param def The default value to use if it was not found
708     * @return The setting
709     */
710    public synchronized Setting<?> getSetting(String key, Setting<?> def) {
711        return getSetting(key, def, Setting.class);
712    }
713
714    /**
715     * Get settings value for a certain key and provide default a value.
716     * @param <T> the setting type
717     * @param key the identifier for the setting
718     * @param def the default value. For each call of getSetting() with a given key, the default value must be the same.
719     * <code>def</code> must not be null, but the value of <code>def</code> can be null.
720     * @param klass the setting type (same as T)
721     * @return the corresponding value if the property has been set before, {@code def} otherwise
722     */
723    @SuppressWarnings("unchecked")
724    @Override
725    public synchronized <T extends Setting<?>> T getSetting(String key, T def, Class<T> klass) {
726        CheckParameterUtil.ensureParameterNotNull(key);
727        CheckParameterUtil.ensureParameterNotNull(def);
728        Setting<?> oldDef = defaultsMap.get(key);
729        if (oldDef != null && oldDef.isNew() && oldDef.getValue() != null && def.getValue() != null && !def.equals(oldDef)) {
730            Logging.info("Defaults for " + key + " differ: " + def + " != " + defaultsMap.get(key));
731        }
732        if (def.getValue() != null || oldDef == null) {
733            Setting<?> defCopy = def.copy();
734            defCopy.setTime(System.currentTimeMillis() / 1000);
735            defCopy.setNew(true);
736            defaultsMap.put(key, defCopy);
737        }
738        Setting<?> prop = settingsMap.get(key);
739        if (klass.isInstance(prop)) {
740            return (T) prop;
741        } else {
742            return def;
743        }
744    }
745
746    @Override
747    public Set<String> getKeySet() {
748        return Collections.unmodifiableSet(settingsMap.keySet());
749    }
750
751    @Override
752    public Map<String, Setting<?>> getAllSettings() {
753        return new TreeMap<>(settingsMap);
754    }
755
756    /**
757     * Gets a map of all currently known defaults
758     * @return The map (key/setting)
759     */
760    public Map<String, Setting<?>> getAllDefaults() {
761        return new TreeMap<>(defaultsMap);
762    }
763
764    /**
765     * Replies the collection of plugin site URLs from where plugin lists can be downloaded.
766     * @return the collection of plugin site URLs
767     * @see #getOnlinePluginSites
768     */
769    public Collection<String> getPluginSites() {
770        return getList("pluginmanager.sites", Collections.singletonList(Config.getUrls().getJOSMWebsite()+"/pluginicons%<?plugins=>"));
771    }
772
773    /**
774     * Returns the list of plugin sites available according to offline mode settings.
775     * @return the list of available plugin sites
776     * @since 8471
777     */
778    public Collection<String> getOnlinePluginSites() {
779        Collection<String> pluginSites = new ArrayList<>(getPluginSites());
780        for (Iterator<String> it = pluginSites.iterator(); it.hasNext();) {
781            try {
782                OnlineResource.JOSM_WEBSITE.checkOfflineAccess(it.next(), Config.getUrls().getJOSMWebsite());
783            } catch (OfflineAccessException ex) {
784                Logging.log(Logging.LEVEL_WARN, ex);
785                it.remove();
786            }
787        }
788        return pluginSites;
789    }
790
791    /**
792     * Sets the collection of plugin site URLs.
793     *
794     * @param sites the site URLs
795     */
796    public void setPluginSites(Collection<String> sites) {
797        putList("pluginmanager.sites", new ArrayList<>(sites));
798    }
799
800    /**
801     * Returns XML describing these preferences.
802     * @param nopass if password must be excluded
803     * @return XML
804     */
805    public String toXML(boolean nopass) {
806        return toXML(settingsMap.entrySet(), nopass, false);
807    }
808
809    /**
810     * Returns XML describing the given preferences.
811     * @param settings preferences settings
812     * @param nopass if password must be excluded
813     * @param defaults true, if default values are converted to XML, false for
814     * regular preferences
815     * @return XML
816     */
817    public String toXML(Collection<Entry<String, Setting<?>>> settings, boolean nopass, boolean defaults) {
818        try (
819            StringWriter sw = new StringWriter();
820            PreferencesWriter prefWriter = new PreferencesWriter(new PrintWriter(sw), nopass, defaults)
821        ) {
822            prefWriter.write(settings);
823            sw.flush();
824            return sw.toString();
825        } catch (IOException e) {
826            Logging.error(e);
827            return null;
828        }
829    }
830
831    /**
832     * Removes and updates obsolete preference settings. If you throw out a once-used preference
833     * setting, add it to the list here with an expiry date (written as comment). If you
834     * see something with an expiry date in the past, remove it from the list.
835     * @param loadedVersion JOSM version when the preferences file was written
836     */
837    private void removeAndUpdateObsolete(int loadedVersion) {
838        Logging.trace("Update obsolete preference keys for version {0}", Integer.toString(loadedVersion));
839        for (Entry<String, String> e : UPDATE_PREF_KEYS.entrySet()) {
840            String oldkey = e.getKey();
841            String newkey = e.getValue();
842            if (settingsMap.containsKey(oldkey)) {
843                Setting<?> value = settingsMap.remove(oldkey);
844                settingsMap.putIfAbsent(newkey, value);
845                Logging.info(tr("Updated preference setting {0} to {1}", oldkey, newkey));
846            }
847        }
848
849        Logging.trace("Remove obsolete preferences for version {0}", Integer.toString(loadedVersion));
850        for (String key : OBSOLETE_PREF_KEYS) {
851            if (settingsMap.containsKey(key)) {
852                settingsMap.remove(key);
853                Logging.info(tr("Removed preference setting {0} since it is no longer used", key));
854            }
855            if (defaultsMap.containsKey(key)) {
856                defaultsMap.remove(key);
857                Logging.info(tr("Removed preference default {0} since it is no longer used", key));
858                modifiedDefault = true;
859            }
860        }
861        for (String key : OBSOLETE_PREF_KEYS_START) {
862            settingsMap.entrySet().stream()
863            .filter(e -> e.getKey().startsWith(key))
864            .collect(Collectors.toSet())
865            .forEach(e -> {
866                String k = e.getKey();
867                if (!KEEP_PREF_KEYS.contains(k)) {
868                    settingsMap.remove(k);
869                    Logging.info(tr("Removed preference setting {0} since it is no longer used", k));
870                }
871            });
872            defaultsMap.entrySet().stream()
873            .filter(e -> e.getKey().startsWith(key))
874            .collect(Collectors.toSet())
875            .forEach(e -> {
876                String k = e.getKey();
877                if (!KEEP_PREF_KEYS.contains(k)) {
878                    defaultsMap.remove(k);
879                    Logging.info(tr("Removed preference default {0} since it is no longer used", k));
880                    modifiedDefault = true;
881                }
882            });
883        }
884        if (modifiedDefault) {
885            try {
886                saveDefaults();
887                Logging.info(tr("Saved updated default preferences."));
888            } catch (IOException ex) {
889                Logging.log(Logging.LEVEL_WARN, tr("Failed to save default preferences."), ex);
890            }
891            modifiedDefault = false;
892        }
893    }
894
895    /**
896     * Enables or not the preferences file auto-save mechanism (save each time a setting is changed).
897     * This behaviour is enabled by default.
898     * @param enable if {@code true}, makes JOSM save preferences file each time a setting is changed
899     * @since 7085
900     */
901    public final void enableSaveOnPut(boolean enable) {
902        synchronized (this) {
903            saveOnPut = enable;
904        }
905    }
906}