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

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

see #15410 - fix deleting color entries; do not display unchanged layer colors (this gets excessive if user opens may gpx files over time)

  • Property svn:eol-style set to native
File size: 64.6 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                if (e.getKey().startsWith(COLOR_PREFIX+"layer."))
645                    continue; // do not add unchanged layer colors
646                StringSetting d = (StringSetting) e.getValue();
647                if (d.getValue() != null) {
648                    all.put(e.getKey().substring(6), d.getValue());
649                }
650            }
651        }
652        for (final Entry<String, Setting<?>> e : settingsMap.entrySet()) {
653            if (e.getKey().startsWith(COLOR_PREFIX) && (e.getValue() instanceof StringSetting)) {
654                all.put(e.getKey().substring(6), ((StringSetting) e.getValue()).getValue());
655            }
656        }
657        return all;
658    }
659
660    /**
661     * Gets an boolean that may be specialized
662     * @param key The basic key
663     * @param specName The sub-key to append to the key
664     * @param def The default value
665     * @return The boolean value or the default value if it could not be parsed
666     * @deprecated use {@link PreferencesUtils#getBoolean(IPreferences, String, String, boolean)}
667     */
668    @Deprecated
669    public synchronized boolean getBoolean(final String key, final String specName, final boolean def) {
670        boolean generic = getBoolean(key, def);
671        String skey = key+'.'+specName;
672        Setting<?> prop = settingsMap.get(skey);
673        if (prop instanceof StringSetting)
674            return Boolean.parseBoolean(((StringSetting) prop).getValue());
675        else
676            return generic;
677    }
678
679    /**
680     * Set a boolean value for a certain setting.
681     * @param key the unique identifier for the setting
682     * @param value The new value
683     * @return {@code true}, if something has changed (i.e. value is different than before)
684     * @see BooleanProperty
685     * @deprecated use {@link IPreferences#putBoolean(String, boolean)}
686     */
687    @Deprecated
688    public boolean put(final String key, final boolean value) {
689        return put(key, Boolean.toString(value));
690    }
691
692    /**
693     * Set a boolean value for a certain setting.
694     * @param key the unique identifier for the setting
695     * @param value The new value
696     * @return {@code true}, if something has changed (i.e. value is different than before)
697     * @see IntegerProperty#put(Integer)
698     * @deprecated use {@link IPreferences#putInt(String, int)}
699     */
700    @Deprecated
701    public boolean putInteger(final String key, final Integer value) {
702        return put(key, Integer.toString(value));
703    }
704
705    /**
706     * Set a boolean value for a certain setting.
707     * @param key the unique identifier for the setting
708     * @param value The new value
709     * @return {@code true}, if something has changed (i.e. value is different than before)
710     * @see DoubleProperty#put(Double)
711     * @deprecated use {@link IPreferences#putDouble(java.lang.String, double)}
712     */
713    @Deprecated
714    public boolean putDouble(final String key, final Double value) {
715        return put(key, Double.toString(value));
716    }
717
718    /**
719     * Set a boolean value for a certain setting.
720     * @param key the unique identifier for the setting
721     * @param value The new value
722     * @return {@code true}, if something has changed (i.e. value is different than before)
723     * @see LongProperty#put(Long)
724     * @deprecated use {@link IPreferences#putLong(java.lang.String, long)}
725     */
726    @Deprecated
727    public boolean putLong(final String key, final Long value) {
728        return put(key, Long.toString(value));
729    }
730
731    /**
732     * Called after every put. In case of a problem, do nothing but output the error in log.
733     * @throws IOException if any I/O error occurs
734     */
735    public synchronized void save() throws IOException {
736        save(getPreferenceFile(), settingsMap.entrySet().stream().filter(NO_DEFAULT_SETTINGS_ENTRY), false);
737    }
738
739    /**
740     * Stores the defaults to the defaults file
741     * @throws IOException If the file could not be saved
742     */
743    public synchronized void saveDefaults() throws IOException {
744        save(getDefaultsCacheFile(), defaultsMap.entrySet().stream(), true);
745    }
746
747    protected void save(File prefFile, Stream<Entry<String, Setting<?>>> settings, boolean defaults) throws IOException {
748        if (!defaults) {
749            /* currently unused, but may help to fix configuration issues in future */
750            putInt("josm.version", Version.getInstance().getVersion());
751
752            updateSystemProperties();
753        }
754
755        File backupFile = new File(prefFile + "_backup");
756
757        // Backup old preferences if there are old preferences
758        if (initSuccessful && prefFile.exists() && prefFile.length() > 0) {
759            Utils.copyFile(prefFile, backupFile);
760        }
761
762        try (PreferencesWriter writer = new PreferencesWriter(
763                new PrintWriter(new File(prefFile + "_tmp"), StandardCharsets.UTF_8.name()), false, defaults)) {
764            writer.write(settings);
765        }
766
767        File tmpFile = new File(prefFile + "_tmp");
768        Utils.copyFile(tmpFile, prefFile);
769        Utils.deleteFile(tmpFile, marktr("Unable to delete temporary file {0}"));
770
771        setCorrectPermissions(prefFile);
772        setCorrectPermissions(backupFile);
773    }
774
775    private static void setCorrectPermissions(File file) {
776        if (!file.setReadable(false, false) && Logging.isTraceEnabled()) {
777            Logging.trace(tr("Unable to set file non-readable {0}", file.getAbsolutePath()));
778        }
779        if (!file.setWritable(false, false) && Logging.isTraceEnabled()) {
780            Logging.trace(tr("Unable to set file non-writable {0}", file.getAbsolutePath()));
781        }
782        if (!file.setExecutable(false, false) && Logging.isTraceEnabled()) {
783            Logging.trace(tr("Unable to set file non-executable {0}", file.getAbsolutePath()));
784        }
785        if (!file.setReadable(true, true) && Logging.isTraceEnabled()) {
786            Logging.trace(tr("Unable to set file readable {0}", file.getAbsolutePath()));
787        }
788        if (!file.setWritable(true, true) && Logging.isTraceEnabled()) {
789            Logging.trace(tr("Unable to set file writable {0}", file.getAbsolutePath()));
790        }
791    }
792
793    /**
794     * Loads preferences from settings file.
795     * @throws IOException if any I/O error occurs while reading the file
796     * @throws SAXException if the settings file does not contain valid XML
797     * @throws XMLStreamException if an XML error occurs while parsing the file (after validation)
798     */
799    protected void load() throws IOException, SAXException, XMLStreamException {
800        File pref = getPreferenceFile();
801        PreferencesReader.validateXML(pref);
802        PreferencesReader reader = new PreferencesReader(pref, false);
803        reader.parse();
804        settingsMap.clear();
805        settingsMap.putAll(reader.getSettings());
806        updateSystemProperties();
807        removeObsolete(reader.getVersion());
808    }
809
810    /**
811     * Loads default preferences from default settings cache file.
812     *
813     * Discards entries older than {@link #MAX_AGE_DEFAULT_PREFERENCES}.
814     *
815     * @throws IOException if any I/O error occurs while reading the file
816     * @throws SAXException if the settings file does not contain valid XML
817     * @throws XMLStreamException if an XML error occurs while parsing the file (after validation)
818     */
819    protected void loadDefaults() throws IOException, XMLStreamException, SAXException {
820        File def = getDefaultsCacheFile();
821        PreferencesReader.validateXML(def);
822        PreferencesReader reader = new PreferencesReader(def, true);
823        reader.parse();
824        defaultsMap.clear();
825        long minTime = System.currentTimeMillis() / 1000 - MAX_AGE_DEFAULT_PREFERENCES;
826        for (Entry<String, Setting<?>> e : reader.getSettings().entrySet()) {
827            if (e.getValue().getTime() >= minTime) {
828                defaultsMap.put(e.getKey(), e.getValue());
829            }
830        }
831    }
832
833    /**
834     * Loads preferences from XML reader.
835     * @param in XML reader
836     * @throws XMLStreamException if any XML stream error occurs
837     * @throws IOException if any I/O error occurs
838     */
839    public void fromXML(Reader in) throws XMLStreamException, IOException {
840        PreferencesReader reader = new PreferencesReader(in, false);
841        reader.parse();
842        settingsMap.clear();
843        settingsMap.putAll(reader.getSettings());
844    }
845
846    /**
847     * Initializes preferences.
848     * @param reset if {@code true}, current settings file is replaced by the default one
849     */
850    public void init(boolean reset) {
851        initSuccessful = false;
852        // get the preferences.
853        File prefDir = getPreferencesDirectory(false);
854        if (prefDir.exists()) {
855            if (!prefDir.isDirectory()) {
856                Logging.warn(tr("Failed to initialize preferences. Preference directory ''{0}'' is not a directory.",
857                        prefDir.getAbsoluteFile()));
858                JOptionPane.showMessageDialog(
859                        Main.parent,
860                        tr("<html>Failed to initialize preferences.<br>Preference directory ''{0}'' is not a directory.</html>",
861                                prefDir.getAbsoluteFile()),
862                        tr("Error"),
863                        JOptionPane.ERROR_MESSAGE
864                );
865                return;
866            }
867        } else {
868            if (!prefDir.mkdirs()) {
869                Logging.warn(tr("Failed to initialize preferences. Failed to create missing preference directory: {0}",
870                        prefDir.getAbsoluteFile()));
871                JOptionPane.showMessageDialog(
872                        Main.parent,
873                        tr("<html>Failed to initialize preferences.<br>Failed to create missing preference directory: {0}</html>",
874                                prefDir.getAbsoluteFile()),
875                        tr("Error"),
876                        JOptionPane.ERROR_MESSAGE
877                );
878                return;
879            }
880        }
881
882        File preferenceFile = getPreferenceFile();
883        try {
884            if (!preferenceFile.exists()) {
885                Logging.info(tr("Missing preference file ''{0}''. Creating a default preference file.", preferenceFile.getAbsoluteFile()));
886                resetToDefault();
887                save();
888            } else if (reset) {
889                File backupFile = new File(prefDir, "preferences.xml.bak");
890                Main.platform.rename(preferenceFile, backupFile);
891                Logging.warn(tr("Replacing existing preference file ''{0}'' with default preference file.", preferenceFile.getAbsoluteFile()));
892                resetToDefault();
893                save();
894            }
895        } catch (IOException e) {
896            Logging.error(e);
897            JOptionPane.showMessageDialog(
898                    Main.parent,
899                    tr("<html>Failed to initialize preferences.<br>Failed to reset preference file to default: {0}</html>",
900                            getPreferenceFile().getAbsoluteFile()),
901                    tr("Error"),
902                    JOptionPane.ERROR_MESSAGE
903            );
904            return;
905        }
906        try {
907            load();
908            initSuccessful = true;
909        } catch (IOException | SAXException | XMLStreamException e) {
910            Logging.error(e);
911            File backupFile = new File(prefDir, "preferences.xml.bak");
912            JOptionPane.showMessageDialog(
913                    Main.parent,
914                    tr("<html>Preferences file had errors.<br> Making backup of old one to <br>{0}<br> " +
915                            "and creating a new default preference file.</html>",
916                            backupFile.getAbsoluteFile()),
917                    tr("Error"),
918                    JOptionPane.ERROR_MESSAGE
919            );
920            Main.platform.rename(preferenceFile, backupFile);
921            try {
922                resetToDefault();
923                save();
924            } catch (IOException e1) {
925                Logging.error(e1);
926                Logging.warn(tr("Failed to initialize preferences. Failed to reset preference file to default: {0}", getPreferenceFile()));
927            }
928        }
929        File def = getDefaultsCacheFile();
930        if (def.exists()) {
931            try {
932                loadDefaults();
933            } catch (IOException | XMLStreamException | SAXException e) {
934                Logging.error(e);
935                Logging.warn(tr("Failed to load defaults cache file: {0}", def));
936                defaultsMap.clear();
937                if (!def.delete()) {
938                    Logging.warn(tr("Failed to delete faulty defaults cache file: {0}", def));
939                }
940            }
941        }
942    }
943
944    /**
945     * Resets the preferences to their initial state. This resets all values and file associations.
946     * The default values and listeners are not removed.
947     * <p>
948     * It is meant to be called before {@link #init(boolean)}
949     * @since 10876
950     */
951    public void resetToInitialState() {
952        resetToDefault();
953        preferencesDir = null;
954        cacheDir = null;
955        userdataDir = null;
956        saveOnPut = true;
957        initSuccessful = false;
958    }
959
960    /**
961     * Reset all values stored in this map to the default values. This clears the preferences.
962     */
963    public final void resetToDefault() {
964        settingsMap.clear();
965    }
966
967    /**
968     * Convenience method for accessing colour preferences.
969     * <p>
970     * To be removed: end of 2016
971     *
972     * @param colName name of the colour
973     * @param def default value
974     * @return a Color object for the configured colour, or the default value if none configured.
975     * @deprecated Use a {@link ColorProperty} instead.
976     */
977    @Deprecated
978    public synchronized Color getColor(String colName, Color def) {
979        return getColor(colName, null, def);
980    }
981
982    /* only for preferences */
983    public synchronized String getColorName(String o) {
984        Matcher m = COLOR_LAYER_PATTERN.matcher(o);
985        if (m.matches()) {
986            return tr("Layer: {0}", tr(I18n.escape(m.group(1))));
987        }
988        String fullKey = COLOR_PREFIX + o;
989        if (colornames.containsKey(fullKey)) {
990            String name = colornames.get(fullKey);
991            Matcher m2 = COLOR_MAPPAINT_PATTERN.matcher(name);
992            if (m2.matches()) {
993                return tr("Paint style {0}: {1}", tr(I18n.escape(m2.group(1))), tr(I18n.escape(m2.group(2))));
994            } else {
995                return tr(I18n.escape(colornames.get(fullKey)));
996            }
997        } else {
998            return fullKey;
999        }
1000    }
1001
1002    /**
1003     * Convenience method for accessing colour preferences.
1004     * <p>
1005     * To be removed: end of 2016
1006     * @param colName name of the colour
1007     * @param specName name of the special colour settings
1008     * @param def default value
1009     * @return a Color object for the configured colour, or the default value if none configured.
1010     * @deprecated Use a {@link ColorProperty} instead.
1011     * You can replace this by: <code>new ColorProperty(colName, def).getChildColor(specName)</code>
1012     */
1013    @Deprecated
1014    public synchronized Color getColor(String colName, String specName, Color def) {
1015        String colKey = ColorProperty.getColorKey(colName);
1016        registerColor(colKey, colName);
1017        String colStr = specName != null ? get(COLOR_PREFIX+specName) : "";
1018        if (colStr.isEmpty()) {
1019            colStr = get(colKey, ColorHelper.color2html(def, true));
1020        }
1021        if (colStr != null && !colStr.isEmpty()) {
1022            return ColorHelper.html2color(colStr);
1023        } else {
1024            return def;
1025        }
1026    }
1027
1028    /**
1029     * Registers a color name conversion for the global color registry.
1030     * @param colKey The key
1031     * @param colName The name of the color.
1032     * @since 10824
1033     */
1034    public void registerColor(String colKey, String colName) {
1035        if (!colKey.equals(colName)) {
1036            colornames.put(colKey, colName);
1037        }
1038    }
1039
1040    /**
1041     * Gets the default color that was registered with the preference
1042     * @param colKey The color name
1043     * @return The color
1044     */
1045    public synchronized Color getDefaultColor(String colKey) {
1046        StringSetting col = Utils.cast(defaultsMap.get(COLOR_PREFIX+colKey), StringSetting.class);
1047        String colStr = col == null ? null : col.getValue();
1048        return colStr == null || colStr.isEmpty() ? null : ColorHelper.html2color(colStr);
1049    }
1050
1051    /**
1052     * Stores a color
1053     * @param colKey The color name
1054     * @param val The color
1055     * @return true if the setting was modified
1056     * @see ColorProperty#put(Color)
1057     */
1058    public synchronized boolean putColor(String colKey, Color val) {
1059        return put(COLOR_PREFIX+colKey, val != null ? ColorHelper.color2html(val, true) : null);
1060    }
1061
1062    /**
1063     * Gets an integer preference
1064     * @param key The preference key
1065     * @param def The default value to use
1066     * @return The integer
1067     * @see IntegerProperty#get()
1068     * @deprecated use {@link IPreferences#getInt(String, int)}
1069     */
1070    @Deprecated
1071    public synchronized int getInteger(String key, int def) {
1072        String v = get(key, Integer.toString(def));
1073        if (v.isEmpty())
1074            return def;
1075
1076        try {
1077            return Integer.parseInt(v);
1078        } catch (NumberFormatException e) {
1079            // fall out
1080            Logging.trace(e);
1081        }
1082        return def;
1083    }
1084
1085    /**
1086     * Gets an integer that may be specialized
1087     * @param key The basic key
1088     * @param specName The sub-key to append to the key
1089     * @param def The default value
1090     * @return The integer value or the default value if it could not be parsed
1091     * @deprecated use {@link PreferencesUtils#getInteger(IPreferences, String, String, int)}
1092     */
1093    @Deprecated
1094    public synchronized int getInteger(String key, String specName, int def) {
1095        String v = get(key+'.'+specName);
1096        if (v.isEmpty())
1097            v = get(key, Integer.toString(def));
1098        if (v.isEmpty())
1099            return def;
1100
1101        try {
1102            return Integer.parseInt(v);
1103        } catch (NumberFormatException e) {
1104            // fall out
1105            Logging.trace(e);
1106        }
1107        return def;
1108    }
1109
1110    /**
1111     * Get a list of values for a certain key
1112     * @param key the identifier for the setting
1113     * @param def the default value.
1114     * @return the corresponding value if the property has been set before, {@code def} otherwise
1115     * @deprecated use {@link IPreferences#getList(java.lang.String, java.util.List)}
1116     */
1117    @Deprecated
1118    public Collection<String> getCollection(String key, Collection<String> def) {
1119        return getSetting(key, ListSetting.create(def), ListSetting.class).getValue();
1120    }
1121
1122    /**
1123     * Get a list of values for a certain key
1124     * @param key the identifier for the setting
1125     * @return the corresponding value if the property has been set before, an empty collection otherwise.
1126     * @deprecated use {@link IPreferences#getList(java.lang.String)}
1127     */
1128    @Deprecated
1129    public Collection<String> getCollection(String key) {
1130        Collection<String> val = getList(key, null);
1131        return val == null ? Collections.<String>emptyList() : val;
1132    }
1133
1134    /**
1135     * Removes a value from a given String collection
1136     * @param key The preference key the collection is stored with
1137     * @param value The value that should be removed in the collection
1138     * @see #getList(String)
1139     * @deprecated use {@link PreferencesUtils#removeFromList(IPreferences, String, String)}
1140     */
1141    @Deprecated
1142    public synchronized void removeFromCollection(String key, String value) {
1143        List<String> a = new ArrayList<>(getList(key, Collections.<String>emptyList()));
1144        a.remove(value);
1145        putList(key, a);
1146    }
1147
1148    /**
1149     * Set a value for a certain setting. The changed setting is saved to the preference file immediately.
1150     * Due to caching mechanisms on modern operating systems and hardware, this shouldn't be a performance problem.
1151     * @param key the unique identifier for the setting
1152     * @param setting the value of the setting. In case it is null, the key-value entry will be removed.
1153     * @return {@code true}, if something has changed (i.e. value is different than before)
1154     */
1155    @Override
1156    public boolean putSetting(final String key, Setting<?> setting) {
1157        CheckParameterUtil.ensureParameterNotNull(key);
1158        if (setting != null && setting.getValue() == null)
1159            throw new IllegalArgumentException("setting argument must not have null value");
1160        Setting<?> settingOld;
1161        Setting<?> settingCopy = null;
1162        synchronized (this) {
1163            if (setting == null) {
1164                settingOld = settingsMap.remove(key);
1165                if (settingOld == null)
1166                    return false;
1167            } else {
1168                settingOld = settingsMap.get(key);
1169                if (setting.equals(settingOld))
1170                    return false;
1171                if (settingOld == null && setting.equals(defaultsMap.get(key)))
1172                    return false;
1173                settingCopy = setting.copy();
1174                settingsMap.put(key, settingCopy);
1175            }
1176            if (saveOnPut) {
1177                try {
1178                    save();
1179                } catch (IOException e) {
1180                    Logging.log(Logging.LEVEL_WARN, tr("Failed to persist preferences to ''{0}''", getPreferenceFile().getAbsoluteFile()), e);
1181                }
1182            }
1183        }
1184        // Call outside of synchronized section in case some listener wait for other thread that wait for preference lock
1185        firePreferenceChanged(key, settingOld, settingCopy);
1186        return true;
1187    }
1188
1189    /**
1190     * Get a setting of any type
1191     * @param key The key for the setting
1192     * @param def The default value to use if it was not found
1193     * @return The setting
1194     */
1195    public synchronized Setting<?> getSetting(String key, Setting<?> def) {
1196        return getSetting(key, def, Setting.class);
1197    }
1198
1199    /**
1200     * Get settings value for a certain key and provide default a value.
1201     * @param <T> the setting type
1202     * @param key the identifier for the setting
1203     * @param def the default value. For each call of getSetting() with a given key, the default value must be the same.
1204     * <code>def</code> must not be null, but the value of <code>def</code> can be null.
1205     * @param klass the setting type (same as T)
1206     * @return the corresponding value if the property has been set before, {@code def} otherwise
1207     */
1208    @SuppressWarnings("unchecked")
1209    @Override
1210    public synchronized <T extends Setting<?>> T getSetting(String key, T def, Class<T> klass) {
1211        CheckParameterUtil.ensureParameterNotNull(key);
1212        CheckParameterUtil.ensureParameterNotNull(def);
1213        Setting<?> oldDef = defaultsMap.get(key);
1214        if (oldDef != null && oldDef.isNew() && oldDef.getValue() != null && def.getValue() != null && !def.equals(oldDef)) {
1215            Logging.info("Defaults for " + key + " differ: " + def + " != " + defaultsMap.get(key));
1216        }
1217        if (def.getValue() != null || oldDef == null) {
1218            Setting<?> defCopy = def.copy();
1219            defCopy.setTime(System.currentTimeMillis() / 1000);
1220            defCopy.setNew(true);
1221            defaultsMap.put(key, defCopy);
1222        }
1223        Setting<?> prop = settingsMap.get(key);
1224        if (klass.isInstance(prop)) {
1225            return (T) prop;
1226        } else {
1227            return def;
1228        }
1229    }
1230
1231    /**
1232     * Put a collection.
1233     * @param key key
1234     * @param value value
1235     * @return {@code true}, if something has changed (i.e. value is different than before)
1236     * @deprecated use {@link IPreferences#putList(java.lang.String, java.util.List)}
1237     */
1238    @Deprecated
1239    public boolean putCollection(String key, Collection<String> value) {
1240        return putSetting(key, value == null ? null : ListSetting.create(value));
1241    }
1242
1243    /**
1244     * Saves at most {@code maxsize} items of collection {@code val}.
1245     * @param key key
1246     * @param maxsize max number of items to save
1247     * @param val value
1248     * @return {@code true}, if something has changed (i.e. value is different than before)
1249     * @deprecated use {@link PreferencesUtils#putListBounded(IPreferences, String, int, List)}
1250     */
1251    @Deprecated
1252    public boolean putCollectionBounded(String key, int maxsize, Collection<String> val) {
1253        List<String> newCollection = new ArrayList<>(Math.min(maxsize, val.size()));
1254        for (String i : val) {
1255            if (newCollection.size() >= maxsize) {
1256                break;
1257            }
1258            newCollection.add(i);
1259        }
1260        return putList(key, newCollection);
1261    }
1262
1263    /**
1264     * Used to read a 2-dimensional array of strings from the preference file.
1265     * If not a single entry could be found, <code>def</code> is returned.
1266     * @param key preference key
1267     * @param def default array value
1268     * @return array value
1269     * @deprecated use {@link #getListOfLists(java.lang.String, java.util.List)}
1270     */
1271    @Deprecated
1272    @SuppressWarnings({ "unchecked", "rawtypes" })
1273    public synchronized Collection<Collection<String>> getArray(String key, Collection<Collection<String>> def) {
1274        ListListSetting val = getSetting(key, ListListSetting.create(def), ListListSetting.class);
1275        return (Collection) val.getValue();
1276    }
1277
1278    /**
1279     * Gets a collection of string collections for the given key
1280     * @param key The key
1281     * @return The collection of string collections or an empty collection as default
1282     * @deprecated use {@link IPreferences#getListOfLists(java.lang.String)}
1283     */
1284    @Deprecated
1285    public Collection<Collection<String>> getArray(String key) {
1286        Collection<Collection<String>> res = getArray(key, null);
1287        return res == null ? Collections.<Collection<String>>emptyList() : res;
1288    }
1289
1290    /**
1291     * Put an array.
1292     * @param key key
1293     * @param value value
1294     * @return {@code true}, if something has changed (i.e. value is different than before)
1295     * @deprecated use {@link IPreferences#putListOfLists(java.lang.String, java.util.List)}
1296     */
1297    @Deprecated
1298    public boolean putArray(String key, Collection<Collection<String>> value) {
1299        return putSetting(key, value == null ? null : ListListSetting.create(value));
1300    }
1301
1302    /**
1303     * Gets a collection of key/value maps.
1304     * @param key The key to search at
1305     * @param def The default value to use
1306     * @return The stored value or the default one if it could not be parsed
1307     * @deprecated use {@link IPreferences#getListOfMaps(java.lang.String, java.util.List)}
1308     */
1309    @Deprecated
1310    public Collection<Map<String, String>> getListOfStructs(String key, Collection<Map<String, String>> def) {
1311        return getSetting(key, new MapListSetting(def == null ? null : new ArrayList<>(def)), MapListSetting.class).getValue();
1312    }
1313
1314    /**
1315     * Stores a list of structs
1316     * @param key The key to store the list in
1317     * @param value A list of key/value maps
1318     * @return <code>true</code> if the value was changed
1319     * @see #getListOfMaps(java.lang.String, java.util.List)
1320     * @deprecated use {@link IPreferences#putListOfMaps(java.lang.String, java.util.List)}
1321     */
1322    @Deprecated
1323    public boolean putListOfStructs(String key, Collection<Map<String, String>> value) {
1324        return putSetting(key, value == null ? null : new MapListSetting(new ArrayList<>(value)));
1325    }
1326
1327    /**
1328     * Annotation used for converting objects to String Maps and vice versa.
1329     * Indicates that a certain field should be considered in the conversion process. Otherwise it is ignored.
1330     *
1331     * @see #serializeStruct(java.lang.Object, java.lang.Class)
1332     * @see #deserializeStruct(java.util.Map, java.lang.Class)
1333     * @deprecated use {@link StructUtils.StructEntry}
1334     */
1335    @Deprecated
1336    @Retention(RetentionPolicy.RUNTIME) // keep annotation at runtime
1337    public @interface pref { }
1338
1339    /**
1340     * Annotation used for converting objects to String Maps.
1341     * Indicates that a certain field should be written to the map, even if the value is the same as the default value.
1342     *
1343     * @see #serializeStruct(java.lang.Object, java.lang.Class)
1344     * @deprecated use {@link StructUtils.WriteExplicitly}
1345     */
1346    @Deprecated
1347    @Retention(RetentionPolicy.RUNTIME) // keep annotation at runtime
1348    public @interface writeExplicitly { }
1349
1350    /**
1351     * Get a list of hashes which are represented by a struct-like class.
1352     * Possible properties are given by fields of the class klass that have the @pref annotation.
1353     * Default constructor is used to initialize the struct objects, properties then override some of these default values.
1354     * @param <T> klass type
1355     * @param key main preference key
1356     * @param klass The struct class
1357     * @return a list of objects of type T or an empty list if nothing was found
1358     * @deprecated use {@link StructUtils#getListOfStructs(IPreferences, String, Class)}
1359     */
1360    @Deprecated
1361    public <T> List<T> getListOfStructs(String key, Class<T> klass) {
1362        return StructUtils.getListOfStructs(this, key, klass);
1363    }
1364
1365    /**
1366     * same as above, but returns def if nothing was found
1367     * @param <T> klass type
1368     * @param key main preference key
1369     * @param def default value
1370     * @param klass The struct class
1371     * @return a list of objects of type T or {@code def} if nothing was found
1372     * @deprecated use {@link StructUtils#getListOfStructs(IPreferences, String, Collection, Class)}
1373     */
1374    @Deprecated
1375    public <T> List<T> getListOfStructs(String key, Collection<T> def, Class<T> klass) {
1376        return StructUtils.getListOfStructs(this, key, def, klass);
1377    }
1378
1379    /**
1380     * Convenience method that saves a MapListSetting which is provided as a collection of objects.
1381     *
1382     * Each object is converted to a <code>Map&lt;String, String&gt;</code> using the fields with {@link pref} annotation.
1383     * The field name is the key and the value will be converted to a string.
1384     *
1385     * Considers only fields that have the @pref annotation.
1386     * In addition it does not write fields with null values. (Thus they are cleared)
1387     * Default values are given by the field values after default constructor has been called.
1388     * Fields equal to the default value are not written unless the field has the @writeExplicitly annotation.
1389     * @param <T> the class,
1390     * @param key main preference key
1391     * @param val the list that is supposed to be saved
1392     * @param klass The struct class
1393     * @return true if something has changed
1394     * @deprecated use {@link StructUtils#putListOfStructs(IPreferences, String, Collection, Class)}
1395     */
1396    @Deprecated
1397    public <T> boolean putListOfStructs(String key, Collection<T> val, Class<T> klass) {
1398        return StructUtils.putListOfStructs(this, key, val, klass);
1399    }
1400
1401    /**
1402     * Convert an object to a String Map, by using field names and values as map key and value.
1403     *
1404     * The field value is converted to a String.
1405     *
1406     * Only fields with annotation {@link pref} are taken into account.
1407     *
1408     * Fields will not be written to the map if the value is null or unchanged
1409     * (compared to an object created with the no-arg-constructor).
1410     * The {@link writeExplicitly} annotation overrides this behavior, i.e. the default value will also be written.
1411     *
1412     * @param <T> the class of the object <code>struct</code>
1413     * @param struct the object to be converted
1414     * @param klass the class T
1415     * @return the resulting map (same data content as <code>struct</code>)
1416     * @deprecated use {@link StructUtils#serializeStruct(java.lang.Object, java.lang.Class)}
1417     */
1418    @Deprecated
1419    public static <T> Map<String, String> serializeStruct(T struct, Class<T> klass) {
1420        return StructUtils.serializeStruct(struct, klass);
1421    }
1422
1423    /**
1424     * Converts a String-Map to an object of a certain class, by comparing map keys to field names of the class and assigning
1425     * map values to the corresponding fields.
1426     *
1427     * The map value (a String) is converted to the field type. Supported types are: boolean, Boolean, int, Integer, double,
1428     * Double, String, Map&lt;String, String&gt; and Map&lt;String, List&lt;String&gt;&gt;.
1429     *
1430     * Only fields with annotation {@link pref} are taken into account.
1431     * @param <T> the class
1432     * @param hash the string map with initial values
1433     * @param klass the class T
1434     * @return an object of class T, initialized as described above
1435     * @deprecated use {@link StructUtils#deserializeStruct(java.util.Map, java.lang.Class)}
1436     */
1437    @Deprecated
1438    public static <T> T deserializeStruct(Map<String, String> hash, Class<T> klass) {
1439        return StructUtils.deserializeStruct(hash, klass);
1440    }
1441
1442    /**
1443     * Gets a map of all settings that are currently stored
1444     * @return The settings
1445     */
1446    public Map<String, Setting<?>> getAllSettings() {
1447        return new TreeMap<>(settingsMap);
1448    }
1449
1450    /**
1451     * Gets a map of all currently known defaults
1452     * @return The map (key/setting)
1453     */
1454    public Map<String, Setting<?>> getAllDefaults() {
1455        return new TreeMap<>(defaultsMap);
1456    }
1457
1458    /**
1459     * Updates system properties with the current values in the preferences.
1460     */
1461    public void updateSystemProperties() {
1462        if ("true".equals(get("prefer.ipv6", "auto")) && !"true".equals(Utils.updateSystemProperty("java.net.preferIPv6Addresses", "true"))) {
1463            // never set this to false, only true!
1464            Logging.info(tr("Try enabling IPv6 network, prefering IPv6 over IPv4 (only works on early startup)."));
1465        }
1466        Utils.updateSystemProperty("http.agent", Version.getInstance().getAgentString());
1467        Utils.updateSystemProperty("user.language", get("language"));
1468        // Workaround to fix a Java bug. This ugly hack comes from Sun bug database: https://bugs.openjdk.java.net/browse/JDK-6292739
1469        // Force AWT toolkit to update its internal preferences (fix #6345).
1470        // Does not work anymore with Java 9, to remove with Java 9 migration
1471        if (Utils.getJavaVersion() < 9 && !GraphicsEnvironment.isHeadless()) {
1472            try {
1473                Field field = Toolkit.class.getDeclaredField("resources");
1474                Utils.setObjectsAccessible(field);
1475                field.set(null, ResourceBundle.getBundle("sun.awt.resources.awt"));
1476            } catch (ReflectiveOperationException | RuntimeException e) { // NOPMD
1477                // Catch RuntimeException in order to catch InaccessibleObjectException, new in Java 9
1478                Logging.warn(e);
1479            }
1480        }
1481        // Possibility to disable SNI (not by default) in case of misconfigured https servers
1482        // See #9875 + http://stackoverflow.com/a/14884941/2257172
1483        // then https://josm.openstreetmap.de/ticket/12152#comment:5 for details
1484        if (getBoolean("jdk.tls.disableSNIExtension", false)) {
1485            Utils.updateSystemProperty("jsse.enableSNIExtension", "false");
1486        }
1487    }
1488
1489    /**
1490     * Replies the collection of plugin site URLs from where plugin lists can be downloaded.
1491     * @return the collection of plugin site URLs
1492     * @see #getOnlinePluginSites
1493     */
1494    public Collection<String> getPluginSites() {
1495        return getList("pluginmanager.sites", Collections.singletonList(Main.getJOSMWebsite()+"/pluginicons%<?plugins=>"));
1496    }
1497
1498    /**
1499     * Returns the list of plugin sites available according to offline mode settings.
1500     * @return the list of available plugin sites
1501     * @since 8471
1502     */
1503    public Collection<String> getOnlinePluginSites() {
1504        Collection<String> pluginSites = new ArrayList<>(getPluginSites());
1505        for (Iterator<String> it = pluginSites.iterator(); it.hasNext();) {
1506            try {
1507                OnlineResource.JOSM_WEBSITE.checkOfflineAccess(it.next(), Main.getJOSMWebsite());
1508            } catch (OfflineAccessException ex) {
1509                Logging.log(Logging.LEVEL_WARN, ex);
1510                it.remove();
1511            }
1512        }
1513        return pluginSites;
1514    }
1515
1516    /**
1517     * Sets the collection of plugin site URLs.
1518     *
1519     * @param sites the site URLs
1520     */
1521    public void setPluginSites(Collection<String> sites) {
1522        putList("pluginmanager.sites", new ArrayList<>(sites));
1523    }
1524
1525    /**
1526     * Returns XML describing these preferences.
1527     * @param nopass if password must be excluded
1528     * @return XML
1529     */
1530    public String toXML(boolean nopass) {
1531        return toXML(settingsMap.entrySet(), nopass, false);
1532    }
1533
1534    /**
1535     * Returns XML describing the given preferences.
1536     * @param settings preferences settings
1537     * @param nopass if password must be excluded
1538     * @param defaults true, if default values are converted to XML, false for
1539     * regular preferences
1540     * @return XML
1541     */
1542    public String toXML(Collection<Entry<String, Setting<?>>> settings, boolean nopass, boolean defaults) {
1543        try (
1544            StringWriter sw = new StringWriter();
1545            PreferencesWriter prefWriter = new PreferencesWriter(new PrintWriter(sw), nopass, defaults)
1546        ) {
1547            prefWriter.write(settings);
1548            sw.flush();
1549            return sw.toString();
1550        } catch (IOException e) {
1551            Logging.error(e);
1552            return null;
1553        }
1554    }
1555
1556    /**
1557     * Removes obsolete preference settings. If you throw out a once-used preference
1558     * setting, add it to the list here with an expiry date (written as comment). If you
1559     * see something with an expiry date in the past, remove it from the list.
1560     * @param loadedVersion JOSM version when the preferences file was written
1561     */
1562    private void removeObsolete(int loadedVersion) {
1563        // drop in March 2017
1564        removeUrlFromEntries(loadedVersion, 10063,
1565                "validator.org.openstreetmap.josm.data.validation.tests.MapCSSTagChecker.entries",
1566                "resource://data/validator/power.mapcss");
1567        // drop in March 2017
1568        if (loadedVersion < 11058) {
1569            migrateOldColorKeys();
1570        }
1571        // drop in September 2017
1572        if (loadedVersion < 11424) {
1573            addNewerDefaultEntry(
1574                    "validator.org.openstreetmap.josm.data.validation.tests.MapCSSTagChecker.entries",
1575                    "resource://data/validator/territories.mapcss");
1576        }
1577
1578        for (String key : OBSOLETE_PREF_KEYS) {
1579            if (settingsMap.containsKey(key)) {
1580                settingsMap.remove(key);
1581                Logging.info(tr("Preference setting {0} has been removed since it is no longer used.", key));
1582            }
1583        }
1584    }
1585
1586    private void migrateOldColorKeys() {
1587        settingsMap.keySet().stream()
1588                .filter(key -> key.startsWith(COLOR_PREFIX))
1589                .flatMap(this::searchOldColorKey)
1590                .collect(Collectors.toList()) // to avoid ConcurrentModificationException
1591                .forEach(entry -> {
1592                    final String oldKey = entry.getKey();
1593                    final String newKey = entry.getValue();
1594                    Logging.info("Migrating old color key {0} => {1}", oldKey, newKey);
1595                    put(newKey, get(oldKey));
1596                    put(oldKey, null);
1597                });
1598    }
1599
1600    private Stream<AbstractMap.SimpleImmutableEntry<String, String>> searchOldColorKey(String key) {
1601        final String newKey = ColorProperty.getColorKey(key.substring(COLOR_PREFIX.length()));
1602        return key.equals(newKey) || settingsMap.containsKey(newKey)
1603                ? Stream.empty()
1604                : Stream.of(new AbstractMap.SimpleImmutableEntry<>(key, newKey));
1605    }
1606
1607    private void removeUrlFromEntries(int loadedVersion, int versionMax, String key, String urlPart) {
1608        if (loadedVersion < versionMax) {
1609            Setting<?> setting = settingsMap.get(key);
1610            if (setting instanceof MapListSetting) {
1611                List<Map<String, String>> l = new LinkedList<>();
1612                boolean modified = false;
1613                for (Map<String, String> map: ((MapListSetting) setting).getValue()) {
1614                    String url = map.get("url");
1615                    if (url != null && url.contains(urlPart)) {
1616                        modified = true;
1617                    } else {
1618                        l.add(map);
1619                    }
1620                }
1621                if (modified) {
1622                    putListOfMaps(key, l);
1623                }
1624            }
1625        }
1626    }
1627
1628    private void addNewerDefaultEntry(String key, final String url) {
1629        Setting<?> setting = settingsMap.get(key);
1630        if (setting instanceof MapListSetting) {
1631            List<Map<String, String>> l = new ArrayList<>(((MapListSetting) setting).getValue());
1632            if (l.stream().noneMatch(x -> x.containsValue(url))) {
1633                ValidatorPrefHelper helper = ValidatorPrefHelper.INSTANCE;
1634                Optional<ExtendedSourceEntry> val = helper.getDefault().stream().filter(x -> url.equals(x.url)).findFirst();
1635                if (val.isPresent()) {
1636                    l.add(helper.serialize(val.get()));
1637                }
1638                putListOfMaps(key, l);
1639            }
1640        }
1641    }
1642
1643    /**
1644     * Enables or not the preferences file auto-save mechanism (save each time a setting is changed).
1645     * This behaviour is enabled by default.
1646     * @param enable if {@code true}, makes JOSM save preferences file each time a setting is changed
1647     * @since 7085
1648     */
1649    public final void enableSaveOnPut(boolean enable) {
1650        synchronized (this) {
1651            saveOnPut = enable;
1652        }
1653    }
1654}
Note: See TracBrowser for help on using the repository browser.