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

Last change on this file since 11527 was 11527, checked in by stoecker, 10 months ago

drop imagery entries which have an id, but are no longer in default list

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