source: josm/trunk/src/org/openstreetmap/josm/data/Preferences.java @ 12945

Last change on this file since 12945 was 12945, checked in by bastiK, 18 months ago

see #15410 - fix display of custom layer color

  • Property svn:eol-style set to native
File size: 64.5 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data;
3
4import static org.openstreetmap.josm.tools.I18n.marktr;
5import static org.openstreetmap.josm.tools.I18n.tr;
6
7import java.awt.Color;
8import java.awt.GraphicsEnvironment;
9import java.awt.Toolkit;
10import java.io.File;
11import java.io.IOException;
12import java.io.PrintWriter;
13import java.io.Reader;
14import java.io.StringWriter;
15import java.lang.annotation.Retention;
16import java.lang.annotation.RetentionPolicy;
17import java.lang.reflect.Field;
18import java.nio.charset.StandardCharsets;
19import java.util.AbstractMap;
20import java.util.ArrayList;
21import java.util.Collection;
22import java.util.Collections;
23import java.util.HashMap;
24import java.util.HashSet;
25import java.util.Iterator;
26import java.util.LinkedList;
27import java.util.List;
28import java.util.Map;
29import java.util.Map.Entry;
30import java.util.Optional;
31import java.util.ResourceBundle;
32import java.util.Set;
33import java.util.SortedMap;
34import java.util.TreeMap;
35import java.util.concurrent.TimeUnit;
36import java.util.function.Predicate;
37import java.util.regex.Matcher;
38import java.util.regex.Pattern;
39import java.util.stream.Collectors;
40import java.util.stream.Stream;
41
42import javax.swing.JOptionPane;
43import javax.xml.stream.XMLStreamException;
44
45import org.openstreetmap.josm.Main;
46import org.openstreetmap.josm.data.preferences.BooleanProperty;
47import org.openstreetmap.josm.data.preferences.ColorProperty;
48import org.openstreetmap.josm.data.preferences.DoubleProperty;
49import org.openstreetmap.josm.data.preferences.IntegerProperty;
50import org.openstreetmap.josm.spi.preferences.ListListSetting;
51import org.openstreetmap.josm.spi.preferences.ListSetting;
52import org.openstreetmap.josm.data.preferences.LongProperty;
53import org.openstreetmap.josm.spi.preferences.MapListSetting;
54import org.openstreetmap.josm.data.preferences.PreferencesReader;
55import org.openstreetmap.josm.data.preferences.PreferencesWriter;
56import org.openstreetmap.josm.spi.preferences.Setting;
57import org.openstreetmap.josm.spi.preferences.StringSetting;
58import org.openstreetmap.josm.data.preferences.sources.ExtendedSourceEntry;
59import org.openstreetmap.josm.data.preferences.sources.ValidatorPrefHelper;
60import org.openstreetmap.josm.io.OfflineAccessException;
61import org.openstreetmap.josm.io.OnlineResource;
62import org.openstreetmap.josm.spi.preferences.AbstractPreferences;
63import org.openstreetmap.josm.spi.preferences.IBaseDirectories;
64import org.openstreetmap.josm.spi.preferences.IPreferences;
65import org.openstreetmap.josm.tools.CheckParameterUtil;
66import org.openstreetmap.josm.tools.ColorHelper;
67import org.openstreetmap.josm.tools.I18n;
68import org.openstreetmap.josm.tools.ListenerList;
69import org.openstreetmap.josm.tools.Logging;
70import org.openstreetmap.josm.tools.Utils;
71import org.xml.sax.SAXException;
72
73/**
74 * This class holds all preferences for JOSM.
75 *
76 * Other classes can register their beloved properties here. All properties will be
77 * saved upon set-access.
78 *
79 * Each property is a key=setting pair, where key is a String and setting can be one of
80 * 4 types:
81 *     string, list, list of lists and list of maps.
82 * In addition, each key has a unique default value that is set when the value is first
83 * accessed using one of the get...() methods. You can use the same preference
84 * key in different parts of the code, but the default value must be the same
85 * everywhere. A default value of null means, the setting has been requested, but
86 * no default value was set. This is used in advanced preferences to present a list
87 * off all possible settings.
88 *
89 * At the moment, you cannot put the empty string for string properties.
90 * put(key, "") means, the property is removed.
91 *
92 * @author imi
93 * @since 74
94 */
95public class Preferences extends AbstractPreferences implements IBaseDirectories {
96
97    private static final String COLOR_PREFIX = "color.";
98    private static final Pattern COLOR_LAYER_PATTERN = Pattern.compile("layer\\.(.+)");
99    private static final Pattern COLOR_MAPPAINT_PATTERN = Pattern.compile("mappaint\\.(.+?)\\.(.+)");
100
101    private static final String[] OBSOLETE_PREF_KEYS = {
102      "imagery.layers.addedIds", /* remove entry after June 2017 */
103      "projection", /* remove entry after Nov. 2017 */
104      "projection.sub", /* remove entry after Nov. 2017 */
105    };
106
107    private static final long MAX_AGE_DEFAULT_PREFERENCES = TimeUnit.DAYS.toSeconds(50);
108
109    /**
110     * Internal storage for the preference directory.
111     * Do not access this variable directly!
112     * @see #getPreferencesDirectory()
113     */
114    private File preferencesDir;
115
116    /**
117     * Internal storage for the cache directory.
118     */
119    private File cacheDir;
120
121    /**
122     * Internal storage for the user data directory.
123     */
124    private File userdataDir;
125
126    /**
127     * Determines if preferences file is saved each time a property is changed.
128     */
129    private boolean saveOnPut = true;
130
131    /**
132     * Maps the setting name to the current value of the setting.
133     * The map must not contain null as key or value. The mapped setting objects
134     * must not have a null value.
135     */
136    protected final SortedMap<String, Setting<?>> settingsMap = new TreeMap<>();
137
138    /**
139     * Maps the setting name to the default value of the setting.
140     * The map must not contain null as key or value. The value of the mapped
141     * setting objects can be null.
142     */
143    protected final SortedMap<String, Setting<?>> defaultsMap = new TreeMap<>();
144
145    private final Predicate<Entry<String, Setting<?>>> NO_DEFAULT_SETTINGS_ENTRY =
146            e -> !e.getValue().equals(defaultsMap.get(e.getKey()));
147
148    /**
149     * Maps color keys to human readable color name
150     */
151    protected final SortedMap<String, String> colornames = new TreeMap<>();
152
153    /**
154     * Indicates whether {@link #init(boolean)} completed successfully.
155     * Used to decide whether to write backup preference file in {@link #save()}
156     */
157    protected boolean initSuccessful;
158
159    /**
160     * Event triggered when a preference entry value changes.
161     * @deprecated use {@link org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent}
162     */
163    @Deprecated
164    public interface PreferenceChangeEvent {
165        /**
166         * Returns the preference key.
167         * @return the preference key
168         */
169        String getKey();
170
171        /**
172         * Returns the old preference value.
173         * @return the old preference value
174         */
175        Setting<?> getOldValue();
176
177        /**
178         * Returns the new preference value.
179         * @return the new preference value
180         */
181        Setting<?> getNewValue();
182    }
183
184    /**
185     * Listener to preference change events.
186     * @since 10600 (functional interface)
187     * @deprecated use {@link org.openstreetmap.josm.spi.preferences.PreferenceChangedListener}
188     */
189    @FunctionalInterface
190    @Deprecated
191    public interface PreferenceChangedListener {
192        /**
193         * Trigerred when a preference entry value changes.
194         * @param e the preference change event
195         */
196        void preferenceChanged(PreferenceChangeEvent e);
197    }
198
199    /**
200     * @deprecated private class is deprecated
201     */
202    @Deprecated
203    private static class DefaultPreferenceChangeEvent implements PreferenceChangeEvent {
204        private final String key;
205        private final Setting<?> oldValue;
206        private final Setting<?> newValue;
207
208        DefaultPreferenceChangeEvent(String key, Setting<?> oldValue, Setting<?> newValue) {
209            this.key = key;
210            this.oldValue = oldValue;
211            this.newValue = newValue;
212        }
213
214        @Override
215        public String getKey() {
216            return key;
217        }
218
219        @Override
220        public Setting<?> getOldValue() {
221            return oldValue;
222        }
223
224        @Override
225        public Setting<?> getNewValue() {
226            return newValue;
227        }
228    }
229
230    private final ListenerList<org.openstreetmap.josm.spi.preferences.PreferenceChangedListener> listeners = ListenerList.create();
231
232    private final HashMap<String, ListenerList<org.openstreetmap.josm.spi.preferences.PreferenceChangedListener>> keyListeners = new HashMap<>();
233
234    /**
235     * @deprecated deprecated private field
236     */
237    @Deprecated
238    private final ListenerList<Preferences.PreferenceChangedListener> listenersDeprecated = ListenerList.create();
239
240    /**
241     * @deprecated deprecated private field
242     */
243    @Deprecated
244    private final HashMap<String, ListenerList<Preferences.PreferenceChangedListener>> keyListenersDeprecated = new HashMap<>();
245
246    /**
247     * Constructs a new {@code Preferences}.
248     */
249    public Preferences() {
250        // Default constructor
251    }
252
253    /**
254     * Constructs a new {@code Preferences} from an existing instance.
255     * @param pref existing preferences to copy
256     * @since 12634
257     */
258    public Preferences(Preferences pref) {
259        settingsMap.putAll(pref.settingsMap);
260        defaultsMap.putAll(pref.defaultsMap);
261        colornames.putAll(pref.colornames);
262    }
263
264    /**
265     * Adds a new preferences listener.
266     * @param listener The listener to add
267     * @since 12881
268     */
269    @Override
270    public void addPreferenceChangeListener(org.openstreetmap.josm.spi.preferences.PreferenceChangedListener listener) {
271        if (listener != null) {
272            listeners.addListener(listener);
273        }
274    }
275
276    /**
277     * Adds a new preferences listener.
278     * @param listener The listener to add
279     * @deprecated use {@link #addPreferenceChangeListener(org.openstreetmap.josm.spi.preferences.PreferenceChangedListener)}
280     */
281    @Deprecated
282    public void addPreferenceChangeListener(Preferences.PreferenceChangedListener listener) {
283        if (listener != null) {
284            listenersDeprecated.addListener(listener);
285        }
286    }
287
288    /**
289     * Removes a preferences listener.
290     * @param listener The listener to remove
291     * @since 12881
292     */
293    @Override
294    public void removePreferenceChangeListener(org.openstreetmap.josm.spi.preferences.PreferenceChangedListener listener) {
295        listeners.removeListener(listener);
296    }
297
298    /**
299     * Removes a preferences listener.
300     * @param listener The listener to remove
301     * @deprecated use {@link #removePreferenceChangeListener(org.openstreetmap.josm.spi.preferences.PreferenceChangedListener)}
302     */
303    @Deprecated
304    public void removePreferenceChangeListener(Preferences.PreferenceChangedListener listener) {
305        listenersDeprecated.removeListener(listener);
306    }
307
308    /**
309     * Adds a listener that only listens to changes in one preference
310     * @param key The preference key to listen to
311     * @param listener The listener to add.
312     * @since 12881
313     */
314    @Override
315    public void addKeyPreferenceChangeListener(String key, org.openstreetmap.josm.spi.preferences.PreferenceChangedListener listener) {
316        listenersForKey(key).addListener(listener);
317    }
318
319    /**
320     * Adds a listener that only listens to changes in one preference
321     * @param key The preference key to listen to
322     * @param listener The listener to add.
323     * @since 10824
324     * @deprecated use
325     * {@link #addKeyPreferenceChangeListener(java.lang.String, org.openstreetmap.josm.spi.preferences.PreferenceChangedListener)}
326     */
327    @Deprecated
328    public void addKeyPreferenceChangeListener(String key, Preferences.PreferenceChangedListener listener) {
329        listenersForKeyDeprecated(key).addListener(listener);
330    }
331
332    /**
333     * Adds a weak listener that only listens to changes in one preference
334     * @param key The preference key to listen to
335     * @param listener The listener to add.
336     * @since 10824
337     */
338    public void addWeakKeyPreferenceChangeListener(String key, org.openstreetmap.josm.spi.preferences.PreferenceChangedListener listener) {
339        listenersForKey(key).addWeakListener(listener);
340    }
341
342    private ListenerList<org.openstreetmap.josm.spi.preferences.PreferenceChangedListener> listenersForKey(String key) {
343        return keyListeners.computeIfAbsent(key, k -> ListenerList.create());
344    }
345
346    /**
347     * @deprecated deprecated private method
348     */
349    @Deprecated
350    private ListenerList<Preferences.PreferenceChangedListener> listenersForKeyDeprecated(String key) {
351        return keyListenersDeprecated.computeIfAbsent(key, k -> ListenerList.create());
352    }
353
354    /**
355     * Removes a listener that only listens to changes in one preference
356     * @param key The preference key to listen to
357     * @param listener The listener to add.
358     * @since 12881
359     */
360    @Override
361    public void removeKeyPreferenceChangeListener(String key, org.openstreetmap.josm.spi.preferences.PreferenceChangedListener listener) {
362        Optional.ofNullable(keyListeners.get(key)).orElseThrow(
363                () -> new IllegalArgumentException("There are no listeners registered for " + key))
364        .removeListener(listener);
365    }
366
367    /**
368     * Removes a listener that only listens to changes in one preference
369     * @param key The preference key to listen to
370     * @param listener The listener to add.
371     * @deprecated use
372     * {@link #removeKeyPreferenceChangeListener(java.lang.String, org.openstreetmap.josm.spi.preferences.PreferenceChangedListener)}
373     */
374    @Deprecated
375    public void removeKeyPreferenceChangeListener(String key, Preferences.PreferenceChangedListener listener) {
376        Optional.ofNullable(keyListenersDeprecated.get(key)).orElseThrow(
377                () -> new IllegalArgumentException("There are no listeners registered for " + key))
378        .removeListener(listener);
379    }
380
381    protected void firePreferenceChanged(String key, Setting<?> oldValue, Setting<?> newValue) {
382        final org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent evt =
383                new org.openstreetmap.josm.spi.preferences.DefaultPreferenceChangeEvent(key, oldValue, newValue);
384        listeners.fireEvent(listener -> listener.preferenceChanged(evt));
385
386        ListenerList<org.openstreetmap.josm.spi.preferences.PreferenceChangedListener> forKey = keyListeners.get(key);
387        if (forKey != null) {
388            forKey.fireEvent(listener -> listener.preferenceChanged(evt));
389        }
390        firePreferenceChangedDeprecated(key, oldValue, newValue);
391    }
392
393    /**
394     * @deprecated deprecated private method
395     */
396    @Deprecated
397    private void firePreferenceChangedDeprecated(String key, Setting<?> oldValue, Setting<?> newValue) {
398        final Preferences.PreferenceChangeEvent evtDeprecated = new Preferences.DefaultPreferenceChangeEvent(key, oldValue, newValue);
399        listenersDeprecated.fireEvent(listener -> listener.preferenceChanged(evtDeprecated));
400
401        ListenerList<Preferences.PreferenceChangedListener> forKeyDeprecated = keyListenersDeprecated.get(key);
402        if (forKeyDeprecated != null) {
403            forKeyDeprecated.fireEvent(listener -> listener.preferenceChanged(evtDeprecated));
404        }
405    }
406
407    /**
408     * Get the base name of the JOSM directories for preferences, cache and
409     * user data.
410     * Default value is "JOSM", unless overridden by system property "josm.dir.name".
411     * @return the base name of the JOSM directories for preferences, cache and
412     * user data
413     */
414    public String getJOSMDirectoryBaseName() {
415        String name = System.getProperty("josm.dir.name");
416        if (name != null)
417            return name;
418        else
419            return "JOSM";
420    }
421
422    /**
423     * Returns the user defined preferences directory, containing the preferences.xml file
424     * @return The user defined preferences directory, containing the preferences.xml file
425     * @since 7834
426     * @deprecated use {@link #getPreferencesDirectory(boolean)}
427     */
428    @Deprecated
429    public File getPreferencesDirectory() {
430        return getPreferencesDirectory(false);
431    }
432
433    @Override
434    public File getPreferencesDirectory(boolean createIfMissing) {
435        if (preferencesDir == null) {
436            String path;
437            path = System.getProperty("josm.pref");
438            if (path != null) {
439                preferencesDir = new File(path).getAbsoluteFile();
440            } else {
441                path = System.getProperty("josm.home");
442                if (path != null) {
443                    preferencesDir = new File(path).getAbsoluteFile();
444                } else {
445                    preferencesDir = Main.platform.getDefaultPrefDirectory();
446                }
447            }
448        }
449        if (createIfMissing && !preferencesDir.exists() && !preferencesDir.mkdirs()) {
450            Logging.warn(tr("Failed to create missing preferences directory: {0}", preferencesDir.getAbsoluteFile()));
451            JOptionPane.showMessageDialog(
452                    Main.parent,
453                    tr("<html>Failed to create missing preferences directory: {0}</html>", preferencesDir.getAbsoluteFile()),
454                    tr("Error"),
455                    JOptionPane.ERROR_MESSAGE
456            );
457        }
458        return preferencesDir;
459    }
460
461    /**
462     * Returns the user data directory, containing autosave, plugins, etc.
463     * Depending on the OS it may be the same directory as preferences directory.
464     * @return The user data directory, containing autosave, plugins, etc.
465     * @since 7834
466     * @deprecated use {@link #getUserDataDirectory(boolean)}
467     */
468    @Deprecated
469    public File getUserDataDirectory() {
470        return getUserDataDirectory(false);
471    }
472
473    @Override
474    public File getUserDataDirectory(boolean createIfMissing) {
475        if (userdataDir == null) {
476            String path;
477            path = System.getProperty("josm.userdata");
478            if (path != null) {
479                userdataDir = new File(path).getAbsoluteFile();
480            } else {
481                path = System.getProperty("josm.home");
482                if (path != null) {
483                    userdataDir = new File(path).getAbsoluteFile();
484                } else {
485                    userdataDir = Main.platform.getDefaultUserDataDirectory();
486                }
487            }
488        }
489        if (createIfMissing && !userdataDir.exists() && !userdataDir.mkdirs()) {
490            Logging.warn(tr("Failed to create missing user data directory: {0}", userdataDir.getAbsoluteFile()));
491            JOptionPane.showMessageDialog(
492                    Main.parent,
493                    tr("<html>Failed to create missing user data directory: {0}</html>", userdataDir.getAbsoluteFile()),
494                    tr("Error"),
495                    JOptionPane.ERROR_MESSAGE
496            );
497        }
498        return userdataDir;
499    }
500
501    /**
502     * Returns the user preferences file (preferences.xml).
503     * @return The user preferences file (preferences.xml)
504     */
505    public File getPreferenceFile() {
506        return new File(getPreferencesDirectory(false), "preferences.xml");
507    }
508
509    /**
510     * Returns the cache file for default preferences.
511     * @return the cache file for default preferences
512     */
513    public File getDefaultsCacheFile() {
514        return new File(getCacheDirectory(true), "default_preferences.xml");
515    }
516
517    /**
518     * Returns the user plugin directory.
519     * @return The user plugin directory
520     */
521    public File getPluginsDirectory() {
522        return new File(getUserDataDirectory(false), "plugins");
523    }
524
525    /**
526     * Get the directory where cached content of any kind should be stored.
527     *
528     * If the directory doesn't exist on the file system, it will be created by this method.
529     *
530     * @return the cache directory
531     * @deprecated use {@link #getCacheDirectory(boolean)}
532     */
533    @Deprecated
534    public File getCacheDirectory() {
535        return getCacheDirectory(true);
536    }
537
538    @Override
539    public File getCacheDirectory(boolean createIfMissing) {
540        if (cacheDir == null) {
541            String path = System.getProperty("josm.cache");
542            if (path != null) {
543                cacheDir = new File(path).getAbsoluteFile();
544            } else {
545                path = System.getProperty("josm.home");
546                if (path != null) {
547                    cacheDir = new File(path, "cache");
548                } else {
549                    path = get("cache.folder", null);
550                    if (path != null) {
551                        cacheDir = new File(path).getAbsoluteFile();
552                    } else {
553                        cacheDir = Main.platform.getDefaultCacheDirectory();
554                    }
555                }
556            }
557        }
558        if (createIfMissing && !cacheDir.exists() && !cacheDir.mkdirs()) {
559            Logging.warn(tr("Failed to create missing cache directory: {0}", cacheDir.getAbsoluteFile()));
560            JOptionPane.showMessageDialog(
561                    Main.parent,
562                    tr("<html>Failed to create missing cache directory: {0}</html>", cacheDir.getAbsoluteFile()),
563                    tr("Error"),
564                    JOptionPane.ERROR_MESSAGE
565            );
566        }
567        return cacheDir;
568    }
569
570    private static void addPossibleResourceDir(Set<String> locations, String s) {
571        if (s != null) {
572            if (!s.endsWith(File.separator)) {
573                s += File.separator;
574            }
575            locations.add(s);
576        }
577    }
578
579    /**
580     * Returns a set of all existing directories where resources could be stored.
581     * @return A set of all existing directories where resources could be stored.
582     */
583    public Collection<String> getAllPossiblePreferenceDirs() {
584        Set<String> locations = new HashSet<>();
585        addPossibleResourceDir(locations, getPreferencesDirectory(false).getPath());
586        addPossibleResourceDir(locations, getUserDataDirectory(false).getPath());
587        addPossibleResourceDir(locations, System.getenv("JOSM_RESOURCES"));
588        addPossibleResourceDir(locations, System.getProperty("josm.resources"));
589        if (Main.isPlatformWindows()) {
590            String appdata = System.getenv("APPDATA");
591            if (appdata != null && System.getenv("ALLUSERSPROFILE") != null
592                    && appdata.lastIndexOf(File.separator) != -1) {
593                appdata = appdata.substring(appdata.lastIndexOf(File.separator));
594                locations.add(new File(new File(System.getenv("ALLUSERSPROFILE"),
595                        appdata), "JOSM").getPath());
596            }
597        } else {
598            locations.add("/usr/local/share/josm/");
599            locations.add("/usr/local/lib/josm/");
600            locations.add("/usr/share/josm/");
601            locations.add("/usr/lib/josm/");
602        }
603        return locations;
604    }
605
606    /**
607     * Gets all normal (string) settings that have a key starting with the prefix
608     * @param prefix The start of the key
609     * @return The key names of the settings
610     */
611    public synchronized Map<String, String> getAllPrefix(final String prefix) {
612        final Map<String, String> all = new TreeMap<>();
613        for (final Entry<String, Setting<?>> e : settingsMap.entrySet()) {
614            if (e.getKey().startsWith(prefix) && (e.getValue() instanceof StringSetting)) {
615                all.put(e.getKey(), ((StringSetting) e.getValue()).getValue());
616            }
617        }
618        return all;
619    }
620
621    /**
622     * Gets all list settings that have a key starting with the prefix
623     * @param prefix The start of the key
624     * @return The key names of the list settings
625     */
626    public synchronized List<String> getAllPrefixCollectionKeys(final String prefix) {
627        final List<String> all = new LinkedList<>();
628        for (Map.Entry<String, Setting<?>> entry : settingsMap.entrySet()) {
629            if (entry.getKey().startsWith(prefix) && entry.getValue() instanceof ListSetting) {
630                all.add(entry.getKey());
631            }
632        }
633        return all;
634    }
635
636    /**
637     * Gets all known colors (preferences starting with the color prefix)
638     * @return All colors
639     */
640    public synchronized Map<String, String> getAllColors() {
641        final Map<String, String> all = new TreeMap<>();
642        for (final Entry<String, Setting<?>> e : defaultsMap.entrySet()) {
643            if (e.getKey().startsWith(COLOR_PREFIX) && e.getValue() instanceof StringSetting) {
644                StringSetting d = (StringSetting) e.getValue();
645                if (d.getValue() != null) {
646                    all.put(e.getKey().substring(6), d.getValue());
647                }
648            }
649        }
650        for (final Entry<String, Setting<?>> e : settingsMap.entrySet()) {
651            if (e.getKey().startsWith(COLOR_PREFIX) && (e.getValue() instanceof StringSetting)) {
652                all.put(e.getKey().substring(6), ((StringSetting) e.getValue()).getValue());
653            }
654        }
655        return all;
656    }
657
658    /**
659     * Gets an boolean that may be specialized
660     * @param key The basic key
661     * @param specName The sub-key to append to the key
662     * @param def The default value
663     * @return The boolean value or the default value if it could not be parsed
664     * @deprecated use {@link PreferencesUtils#getBoolean(IPreferences, String, String, boolean)}
665     */
666    @Deprecated
667    public synchronized boolean getBoolean(final String key, final String specName, final boolean def) {
668        boolean generic = getBoolean(key, def);
669        String skey = key+'.'+specName;
670        Setting<?> prop = settingsMap.get(skey);
671        if (prop instanceof StringSetting)
672            return Boolean.parseBoolean(((StringSetting) prop).getValue());
673        else
674            return generic;
675    }
676
677    /**
678     * Set a boolean value for a certain setting.
679     * @param key the unique identifier for the setting
680     * @param value The new value
681     * @return {@code true}, if something has changed (i.e. value is different than before)
682     * @see BooleanProperty
683     * @deprecated use {@link IPreferences#putBoolean(String, boolean)}
684     */
685    @Deprecated
686    public boolean put(final String key, final boolean value) {
687        return put(key, Boolean.toString(value));
688    }
689
690    /**
691     * Set a boolean value for a certain setting.
692     * @param key the unique identifier for the setting
693     * @param value The new value
694     * @return {@code true}, if something has changed (i.e. value is different than before)
695     * @see IntegerProperty#put(Integer)
696     * @deprecated use {@link IPreferences#putInt(String, int)}
697     */
698    @Deprecated
699    public boolean putInteger(final String key, final Integer value) {
700        return put(key, Integer.toString(value));
701    }
702
703    /**
704     * Set a boolean value for a certain setting.
705     * @param key the unique identifier for the setting
706     * @param value The new value
707     * @return {@code true}, if something has changed (i.e. value is different than before)
708     * @see DoubleProperty#put(Double)
709     * @deprecated use {@link IPreferences#putDouble(java.lang.String, double)}
710     */
711    @Deprecated
712    public boolean putDouble(final String key, final Double value) {
713        return put(key, Double.toString(value));
714    }
715
716    /**
717     * Set a boolean value for a certain setting.
718     * @param key the unique identifier for the setting
719     * @param value The new value
720     * @return {@code true}, if something has changed (i.e. value is different than before)
721     * @see LongProperty#put(Long)
722     * @deprecated use {@link IPreferences#putLong(java.lang.String, long)}
723     */
724    @Deprecated
725    public boolean putLong(final String key, final Long value) {
726        return put(key, Long.toString(value));
727    }
728
729    /**
730     * Called after every put. In case of a problem, do nothing but output the error in log.
731     * @throws IOException if any I/O error occurs
732     */
733    public synchronized void save() throws IOException {
734        save(getPreferenceFile(), settingsMap.entrySet().stream().filter(NO_DEFAULT_SETTINGS_ENTRY), false);
735    }
736
737    /**
738     * Stores the defaults to the defaults file
739     * @throws IOException If the file could not be saved
740     */
741    public synchronized void saveDefaults() throws IOException {
742        save(getDefaultsCacheFile(), defaultsMap.entrySet().stream(), true);
743    }
744
745    protected void save(File prefFile, Stream<Entry<String, Setting<?>>> settings, boolean defaults) throws IOException {
746        if (!defaults) {
747            /* currently unused, but may help to fix configuration issues in future */
748            putInt("josm.version", Version.getInstance().getVersion());
749
750            updateSystemProperties();
751        }
752
753        File backupFile = new File(prefFile + "_backup");
754
755        // Backup old preferences if there are old preferences
756        if (initSuccessful && prefFile.exists() && prefFile.length() > 0) {
757            Utils.copyFile(prefFile, backupFile);
758        }
759
760        try (PreferencesWriter writer = new PreferencesWriter(
761                new PrintWriter(new File(prefFile + "_tmp"), StandardCharsets.UTF_8.name()), false, defaults)) {
762            writer.write(settings);
763        }
764
765        File tmpFile = new File(prefFile + "_tmp");
766        Utils.copyFile(tmpFile, prefFile);
767        Utils.deleteFile(tmpFile, marktr("Unable to delete temporary file {0}"));
768
769        setCorrectPermissions(prefFile);
770        setCorrectPermissions(backupFile);
771    }
772
773    private static void setCorrectPermissions(File file) {
774        if (!file.setReadable(false, false) && Logging.isTraceEnabled()) {
775            Logging.trace(tr("Unable to set file non-readable {0}", file.getAbsolutePath()));
776        }
777        if (!file.setWritable(false, false) && Logging.isTraceEnabled()) {
778            Logging.trace(tr("Unable to set file non-writable {0}", file.getAbsolutePath()));
779        }
780        if (!file.setExecutable(false, false) && Logging.isTraceEnabled()) {
781            Logging.trace(tr("Unable to set file non-executable {0}", file.getAbsolutePath()));
782        }
783        if (!file.setReadable(true, true) && Logging.isTraceEnabled()) {
784            Logging.trace(tr("Unable to set file readable {0}", file.getAbsolutePath()));
785        }
786        if (!file.setWritable(true, true) && Logging.isTraceEnabled()) {
787            Logging.trace(tr("Unable to set file writable {0}", file.getAbsolutePath()));
788        }
789    }
790
791    /**
792     * Loads preferences from settings file.
793     * @throws IOException if any I/O error occurs while reading the file
794     * @throws SAXException if the settings file does not contain valid XML
795     * @throws XMLStreamException if an XML error occurs while parsing the file (after validation)
796     */
797    protected void load() throws IOException, SAXException, XMLStreamException {
798        File pref = getPreferenceFile();
799        PreferencesReader.validateXML(pref);
800        PreferencesReader reader = new PreferencesReader(pref, false);
801        reader.parse();
802        settingsMap.clear();
803        settingsMap.putAll(reader.getSettings());
804        updateSystemProperties();
805        removeObsolete(reader.getVersion());
806    }
807
808    /**
809     * Loads default preferences from default settings cache file.
810     *
811     * Discards entries older than {@link #MAX_AGE_DEFAULT_PREFERENCES}.
812     *
813     * @throws IOException if any I/O error occurs while reading the file
814     * @throws SAXException if the settings file does not contain valid XML
815     * @throws XMLStreamException if an XML error occurs while parsing the file (after validation)
816     */
817    protected void loadDefaults() throws IOException, XMLStreamException, SAXException {
818        File def = getDefaultsCacheFile();
819        PreferencesReader.validateXML(def);
820        PreferencesReader reader = new PreferencesReader(def, true);
821        reader.parse();
822        defaultsMap.clear();
823        long minTime = System.currentTimeMillis() / 1000 - MAX_AGE_DEFAULT_PREFERENCES;
824        for (Entry<String, Setting<?>> e : reader.getSettings().entrySet()) {
825            if (e.getValue().getTime() >= minTime) {
826                defaultsMap.put(e.getKey(), e.getValue());
827            }
828        }
829    }
830
831    /**
832     * Loads preferences from XML reader.
833     * @param in XML reader
834     * @throws XMLStreamException if any XML stream error occurs
835     * @throws IOException if any I/O error occurs
836     */
837    public void fromXML(Reader in) throws XMLStreamException, IOException {
838        PreferencesReader reader = new PreferencesReader(in, false);
839        reader.parse();
840        settingsMap.clear();
841        settingsMap.putAll(reader.getSettings());
842    }
843
844    /**
845     * Initializes preferences.
846     * @param reset if {@code true}, current settings file is replaced by the default one
847     */
848    public void init(boolean reset) {
849        initSuccessful = false;
850        // get the preferences.
851        File prefDir = getPreferencesDirectory(false);
852        if (prefDir.exists()) {
853            if (!prefDir.isDirectory()) {
854                Logging.warn(tr("Failed to initialize preferences. Preference directory ''{0}'' is not a directory.",
855                        prefDir.getAbsoluteFile()));
856                JOptionPane.showMessageDialog(
857                        Main.parent,
858                        tr("<html>Failed to initialize preferences.<br>Preference directory ''{0}'' is not a directory.</html>",
859                                prefDir.getAbsoluteFile()),
860                        tr("Error"),
861                        JOptionPane.ERROR_MESSAGE
862                );
863                return;
864            }
865        } else {
866            if (!prefDir.mkdirs()) {
867                Logging.warn(tr("Failed to initialize preferences. Failed to create missing preference directory: {0}",
868                        prefDir.getAbsoluteFile()));
869                JOptionPane.showMessageDialog(
870                        Main.parent,
871                        tr("<html>Failed to initialize preferences.<br>Failed to create missing preference directory: {0}</html>",
872                                prefDir.getAbsoluteFile()),
873                        tr("Error"),
874                        JOptionPane.ERROR_MESSAGE
875                );
876                return;
877            }
878        }
879
880        File preferenceFile = getPreferenceFile();
881        try {
882            if (!preferenceFile.exists()) {
883                Logging.info(tr("Missing preference file ''{0}''. Creating a default preference file.", preferenceFile.getAbsoluteFile()));
884                resetToDefault();
885                save();
886            } else if (reset) {
887                File backupFile = new File(prefDir, "preferences.xml.bak");
888                Main.platform.rename(preferenceFile, backupFile);
889                Logging.warn(tr("Replacing existing preference file ''{0}'' with default preference file.", preferenceFile.getAbsoluteFile()));
890                resetToDefault();
891                save();
892            }
893        } catch (IOException e) {
894            Logging.error(e);
895            JOptionPane.showMessageDialog(
896                    Main.parent,
897                    tr("<html>Failed to initialize preferences.<br>Failed to reset preference file to default: {0}</html>",
898                            getPreferenceFile().getAbsoluteFile()),
899                    tr("Error"),
900                    JOptionPane.ERROR_MESSAGE
901            );
902            return;
903        }
904        try {
905            load();
906            initSuccessful = true;
907        } catch (IOException | SAXException | XMLStreamException e) {
908            Logging.error(e);
909            File backupFile = new File(prefDir, "preferences.xml.bak");
910            JOptionPane.showMessageDialog(
911                    Main.parent,
912                    tr("<html>Preferences file had errors.<br> Making backup of old one to <br>{0}<br> " +
913                            "and creating a new default preference file.</html>",
914                            backupFile.getAbsoluteFile()),
915                    tr("Error"),
916                    JOptionPane.ERROR_MESSAGE
917            );
918            Main.platform.rename(preferenceFile, backupFile);
919            try {
920                resetToDefault();
921                save();
922            } catch (IOException e1) {
923                Logging.error(e1);
924                Logging.warn(tr("Failed to initialize preferences. Failed to reset preference file to default: {0}", getPreferenceFile()));
925            }
926        }
927        File def = getDefaultsCacheFile();
928        if (def.exists()) {
929            try {
930                loadDefaults();
931            } catch (IOException | XMLStreamException | SAXException e) {
932                Logging.error(e);
933                Logging.warn(tr("Failed to load defaults cache file: {0}", def));
934                defaultsMap.clear();
935                if (!def.delete()) {
936                    Logging.warn(tr("Failed to delete faulty defaults cache file: {0}", def));
937                }
938            }
939        }
940    }
941
942    /**
943     * Resets the preferences to their initial state. This resets all values and file associations.
944     * The default values and listeners are not removed.
945     * <p>
946     * It is meant to be called before {@link #init(boolean)}
947     * @since 10876
948     */
949    public void resetToInitialState() {
950        resetToDefault();
951        preferencesDir = null;
952        cacheDir = null;
953        userdataDir = null;
954        saveOnPut = true;
955        initSuccessful = false;
956    }
957
958    /**
959     * Reset all values stored in this map to the default values. This clears the preferences.
960     */
961    public final void resetToDefault() {
962        settingsMap.clear();
963    }
964
965    /**
966     * Convenience method for accessing colour preferences.
967     * <p>
968     * To be removed: end of 2016
969     *
970     * @param colName name of the colour
971     * @param def default value
972     * @return a Color object for the configured colour, or the default value if none configured.
973     * @deprecated Use a {@link ColorProperty} instead.
974     */
975    @Deprecated
976    public synchronized Color getColor(String colName, Color def) {
977        return getColor(colName, null, def);
978    }
979
980    /* only for preferences */
981    public synchronized String getColorName(String o) {
982        Matcher m = COLOR_LAYER_PATTERN.matcher(o);
983        if (m.matches()) {
984            return tr("Layer: {0}", tr(I18n.escape(m.group(1))));
985        }
986        String fullKey = COLOR_PREFIX + o;
987        if (colornames.containsKey(fullKey)) {
988            String name = colornames.get(fullKey);
989            Matcher m2 = COLOR_MAPPAINT_PATTERN.matcher(name);
990            if (m2.matches()) {
991                return tr("Paint style {0}: {1}", tr(I18n.escape(m2.group(1))), tr(I18n.escape(m2.group(2))));
992            } else {
993                return tr(I18n.escape(colornames.get(fullKey)));
994            }
995        } else {
996            return fullKey;
997        }
998    }
999
1000    /**
1001     * Convenience method for accessing colour preferences.
1002     * <p>
1003     * To be removed: end of 2016
1004     * @param colName name of the colour
1005     * @param specName name of the special colour settings
1006     * @param def default value
1007     * @return a Color object for the configured colour, or the default value if none configured.
1008     * @deprecated Use a {@link ColorProperty} instead.
1009     * You can replace this by: <code>new ColorProperty(colName, def).getChildColor(specName)</code>
1010     */
1011    @Deprecated
1012    public synchronized Color getColor(String colName, String specName, Color def) {
1013        String colKey = ColorProperty.getColorKey(colName);
1014        registerColor(colKey, colName);
1015        String colStr = specName != null ? get(COLOR_PREFIX+specName) : "";
1016        if (colStr.isEmpty()) {
1017            colStr = get(colKey, ColorHelper.color2html(def, true));
1018        }
1019        if (colStr != null && !colStr.isEmpty()) {
1020            return ColorHelper.html2color(colStr);
1021        } else {
1022            return def;
1023        }
1024    }
1025
1026    /**
1027     * Registers a color name conversion for the global color registry.
1028     * @param colKey The key
1029     * @param colName The name of the color.
1030     * @since 10824
1031     */
1032    public void registerColor(String colKey, String colName) {
1033        if (!colKey.equals(colName)) {
1034            colornames.put(colKey, colName);
1035        }
1036    }
1037
1038    /**
1039     * Gets the default color that was registered with the preference
1040     * @param colKey The color name
1041     * @return The color
1042     */
1043    public synchronized Color getDefaultColor(String colKey) {
1044        StringSetting col = Utils.cast(defaultsMap.get(COLOR_PREFIX+colKey), StringSetting.class);
1045        String colStr = col == null ? null : col.getValue();
1046        return colStr == null || colStr.isEmpty() ? null : ColorHelper.html2color(colStr);
1047    }
1048
1049    /**
1050     * Stores a color
1051     * @param colKey The color name
1052     * @param val The color
1053     * @return true if the setting was modified
1054     * @see ColorProperty#put(Color)
1055     */
1056    public synchronized boolean putColor(String colKey, Color val) {
1057        return put(COLOR_PREFIX+colKey, val != null ? ColorHelper.color2html(val, true) : null);
1058    }
1059
1060    /**
1061     * Gets an integer preference
1062     * @param key The preference key
1063     * @param def The default value to use
1064     * @return The integer
1065     * @see IntegerProperty#get()
1066     * @deprecated use {@link IPreferences#getInt(String, int)}
1067     */
1068    @Deprecated
1069    public synchronized int getInteger(String key, int def) {
1070        String v = get(key, Integer.toString(def));
1071        if (v.isEmpty())
1072            return def;
1073
1074        try {
1075            return Integer.parseInt(v);
1076        } catch (NumberFormatException e) {
1077            // fall out
1078            Logging.trace(e);
1079        }
1080        return def;
1081    }
1082
1083    /**
1084     * Gets an integer that may be specialized
1085     * @param key The basic key
1086     * @param specName The sub-key to append to the key
1087     * @param def The default value
1088     * @return The integer value or the default value if it could not be parsed
1089     * @deprecated use {@link PreferencesUtils#getInteger(IPreferences, String, String, int)}
1090     */
1091    @Deprecated
1092    public synchronized int getInteger(String key, String specName, int def) {
1093        String v = get(key+'.'+specName);
1094        if (v.isEmpty())
1095            v = get(key, Integer.toString(def));
1096        if (v.isEmpty())
1097            return def;
1098
1099        try {
1100            return Integer.parseInt(v);
1101        } catch (NumberFormatException e) {
1102            // fall out
1103            Logging.trace(e);
1104        }
1105        return def;
1106    }
1107
1108    /**
1109     * Get a list of values for a certain key
1110     * @param key the identifier for the setting
1111     * @param def the default value.
1112     * @return the corresponding value if the property has been set before, {@code def} otherwise
1113     * @deprecated use {@link IPreferences#getList(java.lang.String, java.util.List)}
1114     */
1115    @Deprecated
1116    public Collection<String> getCollection(String key, Collection<String> def) {
1117        return getSetting(key, ListSetting.create(def), ListSetting.class).getValue();
1118    }
1119
1120    /**
1121     * Get a list of values for a certain key
1122     * @param key the identifier for the setting
1123     * @return the corresponding value if the property has been set before, an empty collection otherwise.
1124     * @deprecated use {@link IPreferences#getList(java.lang.String)}
1125     */
1126    @Deprecated
1127    public Collection<String> getCollection(String key) {
1128        Collection<String> val = getList(key, null);
1129        return val == null ? Collections.<String>emptyList() : val;
1130    }
1131
1132    /**
1133     * Removes a value from a given String collection
1134     * @param key The preference key the collection is stored with
1135     * @param value The value that should be removed in the collection
1136     * @see #getList(String)
1137     * @deprecated use {@link PreferencesUtils#removeFromList(IPreferences, String, String)}
1138     */
1139    @Deprecated
1140    public synchronized void removeFromCollection(String key, String value) {
1141        List<String> a = new ArrayList<>(getList(key, Collections.<String>emptyList()));
1142        a.remove(value);
1143        putList(key, a);
1144    }
1145
1146    /**
1147     * Set a value for a certain setting. The changed setting is saved to the preference file immediately.
1148     * Due to caching mechanisms on modern operating systems and hardware, this shouldn't be a performance problem.
1149     * @param key the unique identifier for the setting
1150     * @param setting the value of the setting. In case it is null, the key-value entry will be removed.
1151     * @return {@code true}, if something has changed (i.e. value is different than before)
1152     */
1153    @Override
1154    public boolean putSetting(final String key, Setting<?> setting) {
1155        CheckParameterUtil.ensureParameterNotNull(key);
1156        if (setting != null && setting.getValue() == null)
1157            throw new IllegalArgumentException("setting argument must not have null value");
1158        Setting<?> settingOld;
1159        Setting<?> settingCopy = null;
1160        synchronized (this) {
1161            if (setting == null) {
1162                settingOld = settingsMap.remove(key);
1163                if (settingOld == null)
1164                    return false;
1165            } else {
1166                settingOld = settingsMap.get(key);
1167                if (setting.equals(settingOld))
1168                    return false;
1169                if (settingOld == null && setting.equals(defaultsMap.get(key)))
1170                    return false;
1171                settingCopy = setting.copy();
1172                settingsMap.put(key, settingCopy);
1173            }
1174            if (saveOnPut) {
1175                try {
1176                    save();
1177                } catch (IOException e) {
1178                    Logging.log(Logging.LEVEL_WARN, tr("Failed to persist preferences to ''{0}''", getPreferenceFile().getAbsoluteFile()), e);
1179                }
1180            }
1181        }
1182        // Call outside of synchronized section in case some listener wait for other thread that wait for preference lock
1183        firePreferenceChanged(key, settingOld, settingCopy);
1184        return true;
1185    }
1186
1187    /**
1188     * Get a setting of any type
1189     * @param key The key for the setting
1190     * @param def The default value to use if it was not found
1191     * @return The setting
1192     */
1193    public synchronized Setting<?> getSetting(String key, Setting<?> def) {
1194        return getSetting(key, def, Setting.class);
1195    }
1196
1197    /**
1198     * Get settings value for a certain key and provide default a value.
1199     * @param <T> the setting type
1200     * @param key the identifier for the setting
1201     * @param def the default value. For each call of getSetting() with a given key, the default value must be the same.
1202     * <code>def</code> must not be null, but the value of <code>def</code> can be null.
1203     * @param klass the setting type (same as T)
1204     * @return the corresponding value if the property has been set before, {@code def} otherwise
1205     */
1206    @SuppressWarnings("unchecked")
1207    @Override
1208    public synchronized <T extends Setting<?>> T getSetting(String key, T def, Class<T> klass) {
1209        CheckParameterUtil.ensureParameterNotNull(key);
1210        CheckParameterUtil.ensureParameterNotNull(def);
1211        Setting<?> oldDef = defaultsMap.get(key);
1212        if (oldDef != null && oldDef.isNew() && oldDef.getValue() != null && def.getValue() != null && !def.equals(oldDef)) {
1213            Logging.info("Defaults for " + key + " differ: " + def + " != " + defaultsMap.get(key));
1214        }
1215        if (def.getValue() != null || oldDef == null) {
1216            Setting<?> defCopy = def.copy();
1217            defCopy.setTime(System.currentTimeMillis() / 1000);
1218            defCopy.setNew(true);
1219            defaultsMap.put(key, defCopy);
1220        }
1221        Setting<?> prop = settingsMap.get(key);
1222        if (klass.isInstance(prop)) {
1223            return (T) prop;
1224        } else {
1225            return def;
1226        }
1227    }
1228
1229    /**
1230     * Put a collection.
1231     * @param key key
1232     * @param value value
1233     * @return {@code true}, if something has changed (i.e. value is different than before)
1234     * @deprecated use {@link IPreferences#putList(java.lang.String, java.util.List)}
1235     */
1236    @Deprecated
1237    public boolean putCollection(String key, Collection<String> value) {
1238        return putSetting(key, value == null ? null : ListSetting.create(value));
1239    }
1240
1241    /**
1242     * Saves at most {@code maxsize} items of collection {@code val}.
1243     * @param key key
1244     * @param maxsize max number of items to save
1245     * @param val value
1246     * @return {@code true}, if something has changed (i.e. value is different than before)
1247     * @deprecated use {@link PreferencesUtils#putListBounded(IPreferences, String, int, List)}
1248     */
1249    @Deprecated
1250    public boolean putCollectionBounded(String key, int maxsize, Collection<String> val) {
1251        List<String> newCollection = new ArrayList<>(Math.min(maxsize, val.size()));
1252        for (String i : val) {
1253            if (newCollection.size() >= maxsize) {
1254                break;
1255            }
1256            newCollection.add(i);
1257        }
1258        return putList(key, newCollection);
1259    }
1260
1261    /**
1262     * Used to read a 2-dimensional array of strings from the preference file.
1263     * If not a single entry could be found, <code>def</code> is returned.
1264     * @param key preference key
1265     * @param def default array value
1266     * @return array value
1267     * @deprecated use {@link #getListOfLists(java.lang.String, java.util.List)}
1268     */
1269    @Deprecated
1270    @SuppressWarnings({ "unchecked", "rawtypes" })
1271    public synchronized Collection<Collection<String>> getArray(String key, Collection<Collection<String>> def) {
1272        ListListSetting val = getSetting(key, ListListSetting.create(def), ListListSetting.class);
1273        return (Collection) val.getValue();
1274    }
1275
1276    /**
1277     * Gets a collection of string collections for the given key
1278     * @param key The key
1279     * @return The collection of string collections or an empty collection as default
1280     * @deprecated use {@link IPreferences#getListOfLists(java.lang.String)}
1281     */
1282    @Deprecated
1283    public Collection<Collection<String>> getArray(String key) {
1284        Collection<Collection<String>> res = getArray(key, null);
1285        return res == null ? Collections.<Collection<String>>emptyList() : res;
1286    }
1287
1288    /**
1289     * Put an array.
1290     * @param key key
1291     * @param value value
1292     * @return {@code true}, if something has changed (i.e. value is different than before)
1293     * @deprecated use {@link IPreferences#putListOfLists(java.lang.String, java.util.List)}
1294     */
1295    @Deprecated
1296    public boolean putArray(String key, Collection<Collection<String>> value) {
1297        return putSetting(key, value == null ? null : ListListSetting.create(value));
1298    }
1299
1300    /**
1301     * Gets a collection of key/value maps.
1302     * @param key The key to search at
1303     * @param def The default value to use
1304     * @return The stored value or the default one if it could not be parsed
1305     * @deprecated use {@link IPreferences#getListOfMaps(java.lang.String, java.util.List)}
1306     */
1307    @Deprecated
1308    public Collection<Map<String, String>> getListOfStructs(String key, Collection<Map<String, String>> def) {
1309        return getSetting(key, new MapListSetting(def == null ? null : new ArrayList<>(def)), MapListSetting.class).getValue();
1310    }
1311
1312    /**
1313     * Stores a list of structs
1314     * @param key The key to store the list in
1315     * @param value A list of key/value maps
1316     * @return <code>true</code> if the value was changed
1317     * @see #getListOfMaps(java.lang.String, java.util.List)
1318     * @deprecated use {@link IPreferences#putListOfMaps(java.lang.String, java.util.List)}
1319     */
1320    @Deprecated
1321    public boolean putListOfStructs(String key, Collection<Map<String, String>> value) {
1322        return putSetting(key, value == null ? null : new MapListSetting(new ArrayList<>(value)));
1323    }
1324
1325    /**
1326     * Annotation used for converting objects to String Maps and vice versa.
1327     * Indicates that a certain field should be considered in the conversion process. Otherwise it is ignored.
1328     *
1329     * @see #serializeStruct(java.lang.Object, java.lang.Class)
1330     * @see #deserializeStruct(java.util.Map, java.lang.Class)
1331     * @deprecated use {@link StructUtils.StructEntry}
1332     */
1333    @Deprecated
1334    @Retention(RetentionPolicy.RUNTIME) // keep annotation at runtime
1335    public @interface pref { }
1336
1337    /**
1338     * Annotation used for converting objects to String Maps.
1339     * Indicates that a certain field should be written to the map, even if the value is the same as the default value.
1340     *
1341     * @see #serializeStruct(java.lang.Object, java.lang.Class)
1342     * @deprecated use {@link StructUtils.WriteExplicitly}
1343     */
1344    @Deprecated
1345    @Retention(RetentionPolicy.RUNTIME) // keep annotation at runtime
1346    public @interface writeExplicitly { }
1347
1348    /**
1349     * Get a list of hashes which are represented by a struct-like class.
1350     * Possible properties are given by fields of the class klass that have the @pref annotation.
1351     * Default constructor is used to initialize the struct objects, properties then override some of these default values.
1352     * @param <T> klass type
1353     * @param key main preference key
1354     * @param klass The struct class
1355     * @return a list of objects of type T or an empty list if nothing was found
1356     * @deprecated use {@link StructUtils#getListOfStructs(IPreferences, String, Class)}
1357     */
1358    @Deprecated
1359    public <T> List<T> getListOfStructs(String key, Class<T> klass) {
1360        return StructUtils.getListOfStructs(this, key, klass);
1361    }
1362
1363    /**
1364     * same as above, but returns def if nothing was found
1365     * @param <T> klass type
1366     * @param key main preference key
1367     * @param def default value
1368     * @param klass The struct class
1369     * @return a list of objects of type T or {@code def} if nothing was found
1370     * @deprecated use {@link StructUtils#getListOfStructs(IPreferences, String, Collection, Class)}
1371     */
1372    @Deprecated
1373    public <T> List<T> getListOfStructs(String key, Collection<T> def, Class<T> klass) {
1374        return StructUtils.getListOfStructs(this, key, def, klass);
1375    }
1376
1377    /**
1378     * Convenience method that saves a MapListSetting which is provided as a collection of objects.
1379     *
1380     * Each object is converted to a <code>Map&lt;String, String&gt;</code> using the fields with {@link pref} annotation.
1381     * The field name is the key and the value will be converted to a string.
1382     *
1383     * Considers only fields that have the @pref annotation.
1384     * In addition it does not write fields with null values. (Thus they are cleared)
1385     * Default values are given by the field values after default constructor has been called.
1386     * Fields equal to the default value are not written unless the field has the @writeExplicitly annotation.
1387     * @param <T> the class,
1388     * @param key main preference key
1389     * @param val the list that is supposed to be saved
1390     * @param klass The struct class
1391     * @return true if something has changed
1392     * @deprecated use {@link StructUtils#putListOfStructs(IPreferences, String, Collection, Class)}
1393     */
1394    @Deprecated
1395    public <T> boolean putListOfStructs(String key, Collection<T> val, Class<T> klass) {
1396        return StructUtils.putListOfStructs(this, key, val, klass);
1397    }
1398
1399    /**
1400     * Convert an object to a String Map, by using field names and values as map key and value.
1401     *
1402     * The field value is converted to a String.
1403     *
1404     * Only fields with annotation {@link pref} are taken into account.
1405     *
1406     * Fields will not be written to the map if the value is null or unchanged
1407     * (compared to an object created with the no-arg-constructor).
1408     * The {@link writeExplicitly} annotation overrides this behavior, i.e. the default value will also be written.
1409     *
1410     * @param <T> the class of the object <code>struct</code>
1411     * @param struct the object to be converted
1412     * @param klass the class T
1413     * @return the resulting map (same data content as <code>struct</code>)
1414     * @deprecated use {@link StructUtils#serializeStruct(java.lang.Object, java.lang.Class)}
1415     */
1416    @Deprecated
1417    public static <T> Map<String, String> serializeStruct(T struct, Class<T> klass) {
1418        return StructUtils.serializeStruct(struct, klass);
1419    }
1420
1421    /**
1422     * Converts a String-Map to an object of a certain class, by comparing map keys to field names of the class and assigning
1423     * map values to the corresponding fields.
1424     *
1425     * The map value (a String) is converted to the field type. Supported types are: boolean, Boolean, int, Integer, double,
1426     * Double, String, Map&lt;String, String&gt; and Map&lt;String, List&lt;String&gt;&gt;.
1427     *
1428     * Only fields with annotation {@link pref} are taken into account.
1429     * @param <T> the class
1430     * @param hash the string map with initial values
1431     * @param klass the class T
1432     * @return an object of class T, initialized as described above
1433     * @deprecated use {@link StructUtils#deserializeStruct(java.util.Map, java.lang.Class)}
1434     */
1435    @Deprecated
1436    public static <T> T deserializeStruct(Map<String, String> hash, Class<T> klass) {
1437        return StructUtils.deserializeStruct(hash, klass);
1438    }
1439
1440    /**
1441     * Gets a map of all settings that are currently stored
1442     * @return The settings
1443     */
1444    public Map<String, Setting<?>> getAllSettings() {
1445        return new TreeMap<>(settingsMap);
1446    }
1447
1448    /**
1449     * Gets a map of all currently known defaults
1450     * @return The map (key/setting)
1451     */
1452    public Map<String, Setting<?>> getAllDefaults() {
1453        return new TreeMap<>(defaultsMap);
1454    }
1455
1456    /**
1457     * Updates system properties with the current values in the preferences.
1458     */
1459    public void updateSystemProperties() {
1460        if ("true".equals(get("prefer.ipv6", "auto")) && !"true".equals(Utils.updateSystemProperty("java.net.preferIPv6Addresses", "true"))) {
1461            // never set this to false, only true!
1462            Logging.info(tr("Try enabling IPv6 network, prefering IPv6 over IPv4 (only works on early startup)."));
1463        }
1464        Utils.updateSystemProperty("http.agent", Version.getInstance().getAgentString());
1465        Utils.updateSystemProperty("user.language", get("language"));
1466        // Workaround to fix a Java bug. This ugly hack comes from Sun bug database: https://bugs.openjdk.java.net/browse/JDK-6292739
1467        // Force AWT toolkit to update its internal preferences (fix #6345).
1468        // Does not work anymore with Java 9, to remove with Java 9 migration
1469        if (Utils.getJavaVersion() < 9 && !GraphicsEnvironment.isHeadless()) {
1470            try {
1471                Field field = Toolkit.class.getDeclaredField("resources");
1472                Utils.setObjectsAccessible(field);
1473                field.set(null, ResourceBundle.getBundle("sun.awt.resources.awt"));
1474            } catch (ReflectiveOperationException | RuntimeException e) { // NOPMD
1475                // Catch RuntimeException in order to catch InaccessibleObjectException, new in Java 9
1476                Logging.warn(e);
1477            }
1478        }
1479        // Possibility to disable SNI (not by default) in case of misconfigured https servers
1480        // See #9875 + http://stackoverflow.com/a/14884941/2257172
1481        // then https://josm.openstreetmap.de/ticket/12152#comment:5 for details
1482        if (getBoolean("jdk.tls.disableSNIExtension", false)) {
1483            Utils.updateSystemProperty("jsse.enableSNIExtension", "false");
1484        }
1485    }
1486
1487    /**
1488     * Replies the collection of plugin site URLs from where plugin lists can be downloaded.
1489     * @return the collection of plugin site URLs
1490     * @see #getOnlinePluginSites
1491     */
1492    public Collection<String> getPluginSites() {
1493        return getList("pluginmanager.sites", Collections.singletonList(Main.getJOSMWebsite()+"/pluginicons%<?plugins=>"));
1494    }
1495
1496    /**
1497     * Returns the list of plugin sites available according to offline mode settings.
1498     * @return the list of available plugin sites
1499     * @since 8471
1500     */
1501    public Collection<String> getOnlinePluginSites() {
1502        Collection<String> pluginSites = new ArrayList<>(getPluginSites());
1503        for (Iterator<String> it = pluginSites.iterator(); it.hasNext();) {
1504            try {
1505                OnlineResource.JOSM_WEBSITE.checkOfflineAccess(it.next(), Main.getJOSMWebsite());
1506            } catch (OfflineAccessException ex) {
1507                Logging.log(Logging.LEVEL_WARN, ex);
1508                it.remove();
1509            }
1510        }
1511        return pluginSites;
1512    }
1513
1514    /**
1515     * Sets the collection of plugin site URLs.
1516     *
1517     * @param sites the site URLs
1518     */
1519    public void setPluginSites(Collection<String> sites) {
1520        putList("pluginmanager.sites", new ArrayList<>(sites));
1521    }
1522
1523    /**
1524     * Returns XML describing these preferences.
1525     * @param nopass if password must be excluded
1526     * @return XML
1527     */
1528    public String toXML(boolean nopass) {
1529        return toXML(settingsMap.entrySet(), nopass, false);
1530    }
1531
1532    /**
1533     * Returns XML describing the given preferences.
1534     * @param settings preferences settings
1535     * @param nopass if password must be excluded
1536     * @param defaults true, if default values are converted to XML, false for
1537     * regular preferences
1538     * @return XML
1539     */
1540    public String toXML(Collection<Entry<String, Setting<?>>> settings, boolean nopass, boolean defaults) {
1541        try (
1542            StringWriter sw = new StringWriter();
1543            PreferencesWriter prefWriter = new PreferencesWriter(new PrintWriter(sw), nopass, defaults)
1544        ) {
1545            prefWriter.write(settings);
1546            sw.flush();
1547            return sw.toString();
1548        } catch (IOException e) {
1549            Logging.error(e);
1550            return null;
1551        }
1552    }
1553
1554    /**
1555     * Removes obsolete preference settings. If you throw out a once-used preference
1556     * setting, add it to the list here with an expiry date (written as comment). If you
1557     * see something with an expiry date in the past, remove it from the list.
1558     * @param loadedVersion JOSM version when the preferences file was written
1559     */
1560    private void removeObsolete(int loadedVersion) {
1561        // drop in March 2017
1562        removeUrlFromEntries(loadedVersion, 10063,
1563                "validator.org.openstreetmap.josm.data.validation.tests.MapCSSTagChecker.entries",
1564                "resource://data/validator/power.mapcss");
1565        // drop in March 2017
1566        if (loadedVersion < 11058) {
1567            migrateOldColorKeys();
1568        }
1569        // drop in September 2017
1570        if (loadedVersion < 11424) {
1571            addNewerDefaultEntry(
1572                    "validator.org.openstreetmap.josm.data.validation.tests.MapCSSTagChecker.entries",
1573                    "resource://data/validator/territories.mapcss");
1574        }
1575
1576        for (String key : OBSOLETE_PREF_KEYS) {
1577            if (settingsMap.containsKey(key)) {
1578                settingsMap.remove(key);
1579                Logging.info(tr("Preference setting {0} has been removed since it is no longer used.", key));
1580            }
1581        }
1582    }
1583
1584    private void migrateOldColorKeys() {
1585        settingsMap.keySet().stream()
1586                .filter(key -> key.startsWith(COLOR_PREFIX))
1587                .flatMap(this::searchOldColorKey)
1588                .collect(Collectors.toList()) // to avoid ConcurrentModificationException
1589                .forEach(entry -> {
1590                    final String oldKey = entry.getKey();
1591                    final String newKey = entry.getValue();
1592                    Logging.info("Migrating old color key {0} => {1}", oldKey, newKey);
1593                    put(newKey, get(oldKey));
1594                    put(oldKey, null);
1595                });
1596    }
1597
1598    private Stream<AbstractMap.SimpleImmutableEntry<String, String>> searchOldColorKey(String key) {
1599        final String newKey = ColorProperty.getColorKey(key.substring(COLOR_PREFIX.length()));
1600        return key.equals(newKey) || settingsMap.containsKey(newKey)
1601                ? Stream.empty()
1602                : Stream.of(new AbstractMap.SimpleImmutableEntry<>(key, newKey));
1603    }
1604
1605    private void removeUrlFromEntries(int loadedVersion, int versionMax, String key, String urlPart) {
1606        if (loadedVersion < versionMax) {
1607            Setting<?> setting = settingsMap.get(key);
1608            if (setting instanceof MapListSetting) {
1609                List<Map<String, String>> l = new LinkedList<>();
1610                boolean modified = false;
1611                for (Map<String, String> map: ((MapListSetting) setting).getValue()) {
1612                    String url = map.get("url");
1613                    if (url != null && url.contains(urlPart)) {
1614                        modified = true;
1615                    } else {
1616                        l.add(map);
1617                    }
1618                }
1619                if (modified) {
1620                    putListOfMaps(key, l);
1621                }
1622            }
1623        }
1624    }
1625
1626    private void addNewerDefaultEntry(String key, final String url) {
1627        Setting<?> setting = settingsMap.get(key);
1628        if (setting instanceof MapListSetting) {
1629            List<Map<String, String>> l = new ArrayList<>(((MapListSetting) setting).getValue());
1630            if (l.stream().noneMatch(x -> x.containsValue(url))) {
1631                ValidatorPrefHelper helper = ValidatorPrefHelper.INSTANCE;
1632                Optional<ExtendedSourceEntry> val = helper.getDefault().stream().filter(x -> url.equals(x.url)).findFirst();
1633                if (val.isPresent()) {
1634                    l.add(helper.serialize(val.get()));
1635                }
1636                putListOfMaps(key, l);
1637            }
1638        }
1639    }
1640
1641    /**
1642     * Enables or not the preferences file auto-save mechanism (save each time a setting is changed).
1643     * This behaviour is enabled by default.
1644     * @param enable if {@code true}, makes JOSM save preferences file each time a setting is changed
1645     * @since 7085
1646     */
1647    public final void enableSaveOnPut(boolean enable) {
1648        synchronized (this) {
1649            saveOnPut = enable;
1650        }
1651    }
1652}
Note: See TracBrowser for help on using the repository browser.