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

Last change on this file since 12881 was 12881, checked in by bastiK, 11 months ago

see #15229 - move remaining classes to spi.preferences package, to make it self-contained

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