001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Color;
007import java.awt.Toolkit;
008import java.io.BufferedReader;
009import java.io.File;
010import java.io.FileOutputStream;
011import java.io.IOException;
012import java.io.InputStream;
013import java.io.OutputStreamWriter;
014import java.io.PrintWriter;
015import java.io.Reader;
016import java.lang.annotation.Retention;
017import java.lang.annotation.RetentionPolicy;
018import java.lang.reflect.Field;
019import java.nio.charset.StandardCharsets;
020import java.nio.file.Files;
021import java.util.ArrayList;
022import java.util.Collection;
023import java.util.Collections;
024import java.util.HashSet;
025import java.util.Iterator;
026import java.util.LinkedHashMap;
027import java.util.LinkedList;
028import java.util.List;
029import java.util.Map;
030import java.util.Map.Entry;
031import java.util.Objects;
032import java.util.ResourceBundle;
033import java.util.Set;
034import java.util.SortedMap;
035import java.util.TreeMap;
036import java.util.concurrent.CopyOnWriteArrayList;
037import java.util.regex.Matcher;
038import java.util.regex.Pattern;
039
040import javax.swing.JOptionPane;
041import javax.swing.UIManager;
042import javax.xml.XMLConstants;
043import javax.xml.stream.XMLInputFactory;
044import javax.xml.stream.XMLStreamConstants;
045import javax.xml.stream.XMLStreamException;
046import javax.xml.stream.XMLStreamReader;
047import javax.xml.transform.stream.StreamSource;
048import javax.xml.validation.Schema;
049import javax.xml.validation.SchemaFactory;
050import javax.xml.validation.Validator;
051
052import org.openstreetmap.josm.Main;
053import org.openstreetmap.josm.data.preferences.ColorProperty;
054import org.openstreetmap.josm.io.CachedFile;
055import org.openstreetmap.josm.io.XmlWriter;
056import org.openstreetmap.josm.tools.CheckParameterUtil;
057import org.openstreetmap.josm.tools.ColorHelper;
058import org.openstreetmap.josm.tools.I18n;
059import org.openstreetmap.josm.tools.Utils;
060import org.xml.sax.SAXException;
061
062/**
063 * This class holds all preferences for JOSM.
064 *
065 * Other classes can register their beloved properties here. All properties will be
066 * saved upon set-access.
067 *
068 * Each property is a key=setting pair, where key is a String and setting can be one of
069 * 4 types:
070 *     string, list, list of lists and list of maps.
071 * In addition, each key has a unique default value that is set when the value is first
072 * accessed using one of the get...() methods. You can use the same preference
073 * key in different parts of the code, but the default value must be the same
074 * everywhere. A default value of null means, the setting has been requested, but
075 * no default value was set. This is used in advanced preferences to present a list
076 * off all possible settings.
077 *
078 * At the moment, you cannot put the empty string for string properties.
079 * put(key, "") means, the property is removed.
080 *
081 * @author imi
082 * @since 74
083 */
084public class Preferences {
085    /**
086     * Internal storage for the preference directory.
087     * Do not access this variable directly!
088     * @see #getPreferencesDirectory()
089     */
090    private File preferencesDir = null;
091
092    /**
093     * Internal storage for the cache directory.
094     */
095    private File cacheDir = null;
096
097    /**
098     * Internal storage for the user data directory.
099     */
100    private File userdataDir = null;
101
102    /**
103     * Determines if preferences file is saved each time a property is changed.
104     */
105    private boolean saveOnPut = true;
106
107    /**
108     * Maps the setting name to the current value of the setting.
109     * The map must not contain null as key or value. The mapped setting objects
110     * must not have a null value.
111     */
112    protected final SortedMap<String, Setting<?>> settingsMap = new TreeMap<>();
113
114    /**
115     * Maps the setting name to the default value of the setting.
116     * The map must not contain null as key or value. The value of the mapped
117     * setting objects can be null.
118     */
119    protected final SortedMap<String, Setting<?>> defaultsMap = new TreeMap<>();
120
121    /**
122     * Maps color keys to human readable color name
123     */
124    protected final SortedMap<String, String> colornames = new TreeMap<>();
125
126    /**
127     * Interface for a preference value.
128     *
129     * Implementations must provide a proper <code>equals</code> method.
130     *
131     * @param <T> the data type for the value
132     */
133    public interface Setting<T> {
134        /**
135         * Returns the value of this setting.
136         *
137         * @return the value of this setting
138         */
139        T getValue();
140
141        /**
142         * Check if the value of this Setting object is equal to the given value.
143         * @param otherVal the other value
144         * @return true if the values are equal
145         */
146        boolean equalVal(T otherVal);
147
148        /**
149         * Clone the current object.
150         * @return an identical copy of the current object
151         */
152        Setting<T> copy();
153
154        /**
155         * Enable usage of the visitor pattern.
156         *
157         * @param visitor the visitor
158         */
159        void visit(SettingVisitor visitor);
160
161        /**
162         * Returns a setting whose value is null.
163         *
164         * Cannot be static, because there is no static inheritance.
165         * @return a Setting object that isn't null itself, but returns null
166         * for {@link #getValue()}
167         */
168        Setting<T> getNullInstance();
169    }
170
171    /**
172     * Base abstract class of all settings, holding the setting value.
173     *
174     * @param <T> The setting type
175     */
176    public abstract static class AbstractSetting<T> implements Setting<T> {
177        protected final T value;
178        /**
179         * Constructs a new {@code AbstractSetting} with the given value
180         * @param value The setting value
181         */
182        public AbstractSetting(T value) {
183            this.value = value;
184        }
185        @Override
186        public T getValue() {
187            return value;
188        }
189        @Override
190        public String toString() {
191            return value != null ? value.toString() : "null";
192        }
193        @Override
194        public int hashCode() {
195            final int prime = 31;
196            int result = 1;
197            result = prime * result + ((value == null) ? 0 : value.hashCode());
198            return result;
199        }
200        @Override
201        public boolean equals(Object obj) {
202            if (this == obj)
203                return true;
204            if (obj == null)
205                return false;
206            if (!(obj instanceof AbstractSetting))
207                return false;
208            AbstractSetting<?> other = (AbstractSetting<?>) obj;
209            if (value == null) {
210                if (other.value != null)
211                    return false;
212            } else if (!value.equals(other.value))
213                return false;
214            return true;
215        }
216    }
217
218    /**
219     * Setting containing a {@link String} value.
220     */
221    public static class StringSetting extends AbstractSetting<String> {
222        /**
223         * Constructs a new {@code StringSetting} with the given value
224         * @param value The setting value
225         */
226        public StringSetting(String value) {
227            super(value);
228        }
229        @Override public boolean equalVal(String otherVal) {
230            if (value == null) return otherVal == null;
231            return value.equals(otherVal);
232        }
233        @Override public StringSetting copy() {
234            return new StringSetting(value);
235        }
236        @Override public void visit(SettingVisitor visitor) {
237            visitor.visit(this);
238        }
239        @Override public StringSetting getNullInstance() {
240            return new StringSetting(null);
241        }
242        @Override
243        public boolean equals(Object other) {
244            if (!(other instanceof StringSetting)) return false;
245            return equalVal(((StringSetting) other).getValue());
246        }
247    }
248
249    /**
250     * Setting containing a {@link List} of {@link String} values.
251     */
252    public static class ListSetting extends AbstractSetting<List<String>> {
253        /**
254         * Constructs a new {@code ListSetting} with the given value
255         * @param value The setting value
256         */
257        public ListSetting(List<String> value) {
258            super(value);
259            consistencyTest();
260        }
261        /**
262         * Convenience factory method.
263         * @param value the value
264         * @return a corresponding ListSetting object
265         */
266        public static ListSetting create(Collection<String> value) {
267            return new ListSetting(value == null ? null : Collections.unmodifiableList(new ArrayList<>(value)));
268        }
269        @Override public boolean equalVal(List<String> otherVal) {
270            return equalCollection(value, otherVal);
271        }
272        public static boolean equalCollection(Collection<String> a, Collection<String> b) {
273            if (a == null) return b == null;
274            if (b == null) return false;
275            if (a.size() != b.size()) return false;
276            Iterator<String> itA = a.iterator();
277            Iterator<String> itB = b.iterator();
278            while (itA.hasNext()) {
279                String aStr = itA.next();
280                String bStr = itB.next();
281                if (!Objects.equals(aStr,bStr)) return false;
282            }
283            return true;
284        }
285        @Override public ListSetting copy() {
286            return ListSetting.create(value);
287        }
288        private void consistencyTest() {
289            if (value != null && value.contains(null))
290                throw new RuntimeException("Error: Null as list element in preference setting");
291        }
292        @Override public void visit(SettingVisitor visitor) {
293            visitor.visit(this);
294        }
295        @Override public ListSetting getNullInstance() {
296            return new ListSetting(null);
297        }
298        @Override
299        public boolean equals(Object other) {
300            if (!(other instanceof ListSetting)) return false;
301            return equalVal(((ListSetting) other).getValue());
302        }
303    }
304
305    /**
306     * Setting containing a {@link List} of {@code List}s of {@link String} values.
307     */
308    public static class ListListSetting extends AbstractSetting<List<List<String>>> {
309
310        /**
311         * Constructs a new {@code ListListSetting} with the given value
312         * @param value The setting value
313         */
314        public ListListSetting(List<List<String>> value) {
315            super(value);
316            consistencyTest();
317        }
318
319        /**
320         * Convenience factory method.
321         * @param value the value
322         * @return a corresponding ListListSetting object
323         */
324        public static ListListSetting create(Collection<Collection<String>> value) {
325            if (value != null) {
326                List<List<String>> valueList = new ArrayList<>(value.size());
327                for (Collection<String> lst : value) {
328                    valueList.add(new ArrayList<>(lst));
329                }
330                return new ListListSetting(valueList);
331            }
332            return new ListListSetting(null);
333        }
334
335        @Override
336        public boolean equalVal(List<List<String>> otherVal) {
337            if (value == null) return otherVal == null;
338            if (otherVal == null) return false;
339            if (value.size() != otherVal.size()) return false;
340            Iterator<List<String>> itA = value.iterator();
341            Iterator<List<String>> itB = otherVal.iterator();
342            while (itA.hasNext()) {
343                if (!ListSetting.equalCollection(itA.next(), itB.next())) return false;
344            }
345            return true;
346        }
347
348        @Override
349        public ListListSetting copy() {
350            if (value == null) return new ListListSetting(null);
351
352            List<List<String>> copy = new ArrayList<>(value.size());
353            for (Collection<String> lst : value) {
354                List<String> lstCopy = new ArrayList<>(lst);
355                copy.add(Collections.unmodifiableList(lstCopy));
356            }
357            return new ListListSetting(Collections.unmodifiableList(copy));
358        }
359
360        private void consistencyTest() {
361            if (value == null) return;
362            if (value.contains(null)) throw new RuntimeException("Error: Null as list element in preference setting");
363            for (Collection<String> lst : value) {
364                if (lst.contains(null)) throw new RuntimeException("Error: Null as inner list element in preference setting");
365            }
366        }
367
368        @Override
369        public void visit(SettingVisitor visitor) {
370            visitor.visit(this);
371        }
372
373        @Override
374        public ListListSetting getNullInstance() {
375            return new ListListSetting(null);
376        }
377
378        @Override
379        public boolean equals(Object other) {
380            if (!(other instanceof ListListSetting)) return false;
381            return equalVal(((ListListSetting) other).getValue());
382        }
383    }
384
385    /**
386     * Setting containing a {@link List} of {@link Map}s of {@link String} values.
387     */
388    public static class MapListSetting extends AbstractSetting<List<Map<String, String>>> {
389
390        /**
391         * Constructs a new {@code MapListSetting} with the given value
392         * @param value The setting value
393         */
394        public MapListSetting(List<Map<String, String>> value) {
395            super(value);
396            consistencyTest();
397        }
398
399        @Override
400        public boolean equalVal(List<Map<String, String>> otherVal) {
401            if (value == null) return otherVal == null;
402            if (otherVal == null) return false;
403            if (value.size() != otherVal.size()) return false;
404            Iterator<Map<String, String>> itA = value.iterator();
405            Iterator<Map<String, String>> itB = otherVal.iterator();
406            while (itA.hasNext()) {
407                if (!equalMap(itA.next(), itB.next())) return false;
408            }
409            return true;
410        }
411
412        private static boolean equalMap(Map<String, String> a, Map<String, String> b) {
413            if (a == null) return b == null;
414            if (b == null) return false;
415            if (a.size() != b.size()) return false;
416            for (Entry<String, String> e : a.entrySet()) {
417                if (!Objects.equals(e.getValue(), b.get(e.getKey()))) return false;
418            }
419            return true;
420        }
421
422        @Override
423        public MapListSetting copy() {
424            if (value == null) return new MapListSetting(null);
425            List<Map<String, String>> copy = new ArrayList<>(value.size());
426            for (Map<String, String> map : value) {
427                Map<String, String> mapCopy = new LinkedHashMap<>(map);
428                copy.add(Collections.unmodifiableMap(mapCopy));
429            }
430            return new MapListSetting(Collections.unmodifiableList(copy));
431        }
432
433        private void consistencyTest() {
434            if (value == null) return;
435            if (value.contains(null)) throw new RuntimeException("Error: Null as list element in preference setting");
436            for (Map<String, String> map : value) {
437                if (map.keySet().contains(null)) throw new RuntimeException("Error: Null as map key in preference setting");
438                if (map.values().contains(null)) throw new RuntimeException("Error: Null as map value in preference setting");
439            }
440        }
441
442        @Override
443        public void visit(SettingVisitor visitor) {
444            visitor.visit(this);
445        }
446
447        @Override
448        public MapListSetting getNullInstance() {
449            return new MapListSetting(null);
450        }
451
452        @Override
453        public boolean equals(Object other) {
454            if (!(other instanceof MapListSetting)) return false;
455            return equalVal(((MapListSetting) other).getValue());
456        }
457    }
458
459    public interface SettingVisitor {
460        void visit(StringSetting setting);
461        void visit(ListSetting value);
462        void visit(ListListSetting value);
463        void visit(MapListSetting value);
464    }
465
466    public interface PreferenceChangeEvent {
467        String getKey();
468        Setting<?> getOldValue();
469        Setting<?> getNewValue();
470    }
471
472    public interface PreferenceChangedListener {
473        void preferenceChanged(PreferenceChangeEvent e);
474    }
475
476    private static class DefaultPreferenceChangeEvent implements PreferenceChangeEvent {
477        private final String key;
478        private final Setting<?> oldValue;
479        private final Setting<?> newValue;
480
481        public DefaultPreferenceChangeEvent(String key, Setting<?> oldValue, Setting<?> newValue) {
482            this.key = key;
483            this.oldValue = oldValue;
484            this.newValue = newValue;
485        }
486
487        @Override
488        public String getKey() {
489            return key;
490        }
491
492        @Override
493        public Setting<?> getOldValue() {
494            return oldValue;
495        }
496
497        @Override
498        public Setting<?> getNewValue() {
499            return newValue;
500        }
501    }
502
503    public interface ColorKey {
504        String getColorName();
505        String getSpecialName();
506        Color getDefaultValue();
507    }
508
509    private final CopyOnWriteArrayList<PreferenceChangedListener> listeners = new CopyOnWriteArrayList<>();
510
511    /**
512     * Adds a new preferences listener.
513     * @param listener The listener to add
514     */
515    public void addPreferenceChangeListener(PreferenceChangedListener listener) {
516        if (listener != null) {
517            listeners.addIfAbsent(listener);
518        }
519    }
520
521    /**
522     * Removes a preferences listener.
523     * @param listener The listener to remove
524     */
525    public void removePreferenceChangeListener(PreferenceChangedListener listener) {
526        listeners.remove(listener);
527    }
528
529    protected void firePreferenceChanged(String key, Setting<?> oldValue, Setting<?> newValue) {
530        PreferenceChangeEvent evt = new DefaultPreferenceChangeEvent(key, oldValue, newValue);
531        for (PreferenceChangedListener l : listeners) {
532            l.preferenceChanged(evt);
533        }
534    }
535
536    /**
537     * Returns the location of the user defined preferences directory
538     * @return The location of the user defined preferences directory
539     * @deprecated use #getPreferencesDirectory() to access preferences directory
540     * or #getUserDataDirectory to access user data directory
541     */
542    @Deprecated
543    public String getPreferencesDir() {
544        final String path = getPreferencesDirectory().getPath();
545        if (path.endsWith(File.separator))
546            return path;
547        return path + File.separator;
548    }
549
550    /**
551     * Returns the user defined preferences directory, containing the preferences.xml file
552     * @return The user defined preferences directory, containing the preferences.xml file
553     * @since 7834
554     */
555    public File getPreferencesDirectory() {
556        if (preferencesDir != null)
557            return preferencesDir;
558        String path;
559        path = System.getProperty("josm.pref");
560        if (path != null) {
561            preferencesDir = new File(path).getAbsoluteFile();
562        } else {
563            path = System.getProperty("josm.home");
564            if (path != null) {
565                preferencesDir = new File(path).getAbsoluteFile();
566            } else {
567                preferencesDir = Main.platform.getDefaultPrefDirectory();
568            }
569        }
570        return preferencesDir;
571    }
572
573    /**
574     * Returns the user data directory, containing autosave, plugins, etc.
575     * Depending on the OS it may be the same directory as preferences directory.
576     * @return The user data directory, containing autosave, plugins, etc.
577     * @since 7834
578     */
579    public File getUserDataDirectory() {
580        if (userdataDir != null)
581            return userdataDir;
582        String path;
583        path = System.getProperty("josm.userdata");
584        if (path != null) {
585            userdataDir = new File(path).getAbsoluteFile();
586        } else {
587            path = System.getProperty("josm.home");
588            if (path != null) {
589                userdataDir = new File(path).getAbsoluteFile();
590            } else {
591                userdataDir = Main.platform.getDefaultUserDataDirectory();
592            }
593        }
594        return userdataDir;
595    }
596
597    /**
598     * Returns the user preferences file (preferences.xml)
599     * @return The user preferences file (preferences.xml)
600     */
601    public File getPreferenceFile() {
602        return new File(getPreferencesDirectory(), "preferences.xml");
603    }
604
605    /**
606     * Returns the user plugin directory
607     * @return The user plugin directory
608     */
609    public File getPluginsDirectory() {
610        return new File(getUserDataDirectory(), "plugins");
611    }
612
613    /**
614     * Get the directory where cached content of any kind should be stored.
615     *
616     * If the directory doesn't exist on the file system, it will be created
617     * by this method.
618     *
619     * @return the cache directory
620     */
621    public File getCacheDirectory() {
622        if (cacheDir != null)
623            return cacheDir;
624        String path = System.getProperty("josm.cache");
625        if (path != null) {
626            cacheDir = new File(path).getAbsoluteFile();
627        } else {
628            path = System.getProperty("josm.home");
629            if (path != null) {
630                cacheDir = new File(path, "cache");
631            } else {
632                path = get("cache.folder", null);
633                if (path != null) {
634                    cacheDir = new File(path).getAbsoluteFile();
635                } else {
636                    cacheDir = Main.platform.getDefaultCacheDirectory();
637                }
638            }
639        }
640        if (!cacheDir.exists() && !cacheDir.mkdirs()) {
641            Main.warn(tr("Failed to create missing cache directory: {0}", cacheDir.getAbsoluteFile()));
642            JOptionPane.showMessageDialog(
643                    Main.parent,
644                    tr("<html>Failed to create missing cache directory: {0}</html>", cacheDir.getAbsoluteFile()),
645                    tr("Error"),
646                    JOptionPane.ERROR_MESSAGE
647            );
648        }
649        return cacheDir;
650    }
651
652    private void addPossibleResourceDir(Set<String> locations, String s) {
653        if (s != null) {
654            if (!s.endsWith(File.separator)) {
655                s += File.separator;
656            }
657            locations.add(s);
658        }
659    }
660
661    /**
662     * Returns a set of all existing directories where resources could be stored.
663     * @return A set of all existing directories where resources could be stored.
664     */
665    public Collection<String> getAllPossiblePreferenceDirs() {
666        Set<String> locations = new HashSet<>();
667        addPossibleResourceDir(locations, getPreferencesDirectory().getPath());
668        addPossibleResourceDir(locations, getUserDataDirectory().getPath());
669        addPossibleResourceDir(locations, System.getenv("JOSM_RESOURCES"));
670        addPossibleResourceDir(locations, System.getProperty("josm.resources"));
671        if (Main.isPlatformWindows()) {
672            String appdata = System.getenv("APPDATA");
673            if (System.getenv("ALLUSERSPROFILE") != null && appdata != null
674                    && appdata.lastIndexOf(File.separator) != -1) {
675                appdata = appdata.substring(appdata.lastIndexOf(File.separator));
676                locations.add(new File(new File(System.getenv("ALLUSERSPROFILE"),
677                        appdata), "JOSM").getPath());
678            }
679        } else {
680            locations.add("/usr/local/share/josm/");
681            locations.add("/usr/local/lib/josm/");
682            locations.add("/usr/share/josm/");
683            locations.add("/usr/lib/josm/");
684        }
685        return locations;
686    }
687
688    /**
689     * Get settings value for a certain key.
690     * @param key the identifier for the setting
691     * @return "" if there is nothing set for the preference key,
692     *  the corresponding value otherwise. The result is not null.
693     */
694    public synchronized String get(final String key) {
695        String value = get(key, null);
696        return value == null ? "" : value;
697    }
698
699    /**
700     * Get settings value for a certain key and provide default a value.
701     * @param key the identifier for the setting
702     * @param def the default value. For each call of get() with a given key, the
703     *  default value must be the same.
704     * @return the corresponding value if the property has been set before,
705     *  def otherwise
706     */
707    public synchronized String get(final String key, final String def) {
708        return getSetting(key, new StringSetting(def), StringSetting.class).getValue();
709    }
710
711    public synchronized Map<String, String> getAllPrefix(final String prefix) {
712        final Map<String,String> all = new TreeMap<>();
713        for (final Entry<String,Setting<?>> e : settingsMap.entrySet()) {
714            if (e.getKey().startsWith(prefix) && (e.getValue() instanceof StringSetting)) {
715                all.put(e.getKey(), ((StringSetting) e.getValue()).getValue());
716            }
717        }
718        return all;
719    }
720
721    public synchronized List<String> getAllPrefixCollectionKeys(final String prefix) {
722        final List<String> all = new LinkedList<>();
723        for (Map.Entry<String, Setting<?>> entry : settingsMap.entrySet()) {
724            if (entry.getKey().startsWith(prefix) && entry.getValue() instanceof ListSetting) {
725                all.add(entry.getKey());
726            }
727        }
728        return all;
729    }
730
731    public synchronized Map<String, String> getAllColors() {
732        final Map<String,String> all = new TreeMap<>();
733        for (final Entry<String,Setting<?>> e : defaultsMap.entrySet()) {
734            if (e.getKey().startsWith("color.") && e.getValue() instanceof StringSetting) {
735                StringSetting d = (StringSetting) e.getValue();
736                if (d.getValue() != null) {
737                    all.put(e.getKey().substring(6), d.getValue());
738                }
739            }
740        }
741        for (final Entry<String,Setting<?>> e : settingsMap.entrySet()) {
742            if (e.getKey().startsWith("color.") && (e.getValue() instanceof StringSetting)) {
743                all.put(e.getKey().substring(6), ((StringSetting) e.getValue()).getValue());
744            }
745        }
746        return all;
747    }
748
749    public synchronized boolean getBoolean(final String key) {
750        String s = get(key, null);
751        return s == null ? false : Boolean.parseBoolean(s);
752    }
753
754    public synchronized boolean getBoolean(final String key, final boolean def) {
755        return Boolean.parseBoolean(get(key, Boolean.toString(def)));
756    }
757
758    public synchronized boolean getBoolean(final String key, final String specName, final boolean def) {
759        boolean generic = getBoolean(key, def);
760        String skey = key+"."+specName;
761        Setting<?> prop = settingsMap.get(skey);
762        if (prop instanceof StringSetting)
763            return Boolean.parseBoolean(((StringSetting)prop).getValue());
764        else
765            return generic;
766    }
767
768    /**
769     * Set a value for a certain setting.
770     * @param key the unique identifier for the setting
771     * @param value the value of the setting. Can be null or "" which both removes
772     *  the key-value entry.
773     * @return true, if something has changed (i.e. value is different than before)
774     */
775    public boolean put(final String key, String value) {
776        if(value != null && value.length() == 0) {
777            value = null;
778        }
779        return putSetting(key, value == null ? null : new StringSetting(value));
780    }
781
782    public boolean put(final String key, final boolean value) {
783        return put(key, Boolean.toString(value));
784    }
785
786    public boolean putInteger(final String key, final Integer value) {
787        return put(key, Integer.toString(value));
788    }
789
790    public boolean putDouble(final String key, final Double value) {
791        return put(key, Double.toString(value));
792    }
793
794    public boolean putLong(final String key, final Long value) {
795        return put(key, Long.toString(value));
796    }
797
798    /**
799     * Called after every put. In case of a problem, do nothing but output the error in log.
800     */
801    public void save() throws IOException {
802        /* currently unused, but may help to fix configuration issues in future */
803        putInteger("josm.version", Version.getInstance().getVersion());
804
805        updateSystemProperties();
806
807        File prefFile = getPreferenceFile();
808        File backupFile = new File(prefFile + "_backup");
809
810        // Backup old preferences if there are old preferences
811        if (prefFile.exists()) {
812            Utils.copyFile(prefFile, backupFile);
813        }
814
815        try (PrintWriter out = new PrintWriter(new OutputStreamWriter(
816                new FileOutputStream(prefFile + "_tmp"), StandardCharsets.UTF_8), false)) {
817            out.print(toXML(false));
818        }
819
820        File tmpFile = new File(prefFile + "_tmp");
821        Utils.copyFile(tmpFile, prefFile);
822        tmpFile.delete();
823
824        setCorrectPermissions(prefFile);
825        setCorrectPermissions(backupFile);
826    }
827
828    private void setCorrectPermissions(File file) {
829        file.setReadable(false, false);
830        file.setWritable(false, false);
831        file.setExecutable(false, false);
832        file.setReadable(true, true);
833        file.setWritable(true, true);
834    }
835
836    /**
837     * Loads preferences from settings file.
838     * @throws IOException if any I/O error occurs while reading the file
839     * @throws SAXException if the settings file does not contain valid XML
840     * @throws XMLStreamException if an XML error occurs while parsing the file (after validation)
841     */
842    public void load() throws IOException, SAXException, XMLStreamException {
843        settingsMap.clear();
844        File pref = getPreferenceFile();
845        try (BufferedReader in = Files.newBufferedReader(pref.toPath(), StandardCharsets.UTF_8)) {
846            validateXML(in);
847        }
848        try (BufferedReader in = Files.newBufferedReader(pref.toPath(), StandardCharsets.UTF_8)) {
849            fromXML(in);
850        }
851        updateSystemProperties();
852        removeObsolete();
853    }
854
855    /**
856     * Initializes preferences.
857     * @param reset if {@code true}, current settings file is replaced by the default one
858     */
859    public void init(boolean reset) {
860        // get the preferences.
861        File prefDir = getPreferencesDirectory();
862        if (prefDir.exists()) {
863            if(!prefDir.isDirectory()) {
864                Main.warn(tr("Failed to initialize preferences. Preference directory ''{0}'' is not a directory.", prefDir.getAbsoluteFile()));
865                JOptionPane.showMessageDialog(
866                        Main.parent,
867                        tr("<html>Failed to initialize preferences.<br>Preference directory ''{0}'' is not a directory.</html>", prefDir.getAbsoluteFile()),
868                        tr("Error"),
869                        JOptionPane.ERROR_MESSAGE
870                );
871                return;
872            }
873        } else {
874            if (! prefDir.mkdirs()) {
875                Main.warn(tr("Failed to initialize preferences. Failed to create missing preference directory: {0}", prefDir.getAbsoluteFile()));
876                JOptionPane.showMessageDialog(
877                        Main.parent,
878                        tr("<html>Failed to initialize preferences.<br>Failed to create missing preference directory: {0}</html>",prefDir.getAbsoluteFile()),
879                        tr("Error"),
880                        JOptionPane.ERROR_MESSAGE
881                );
882                return;
883            }
884        }
885
886        File preferenceFile = getPreferenceFile();
887        try {
888            if (!preferenceFile.exists()) {
889                Main.info(tr("Missing preference file ''{0}''. Creating a default preference file.", preferenceFile.getAbsoluteFile()));
890                resetToDefault();
891                save();
892            } else if (reset) {
893                Main.warn(tr("Replacing existing preference file ''{0}'' with default preference file.", preferenceFile.getAbsoluteFile()));
894                resetToDefault();
895                save();
896            }
897        } catch(IOException e) {
898            Main.error(e);
899            JOptionPane.showMessageDialog(
900                    Main.parent,
901                    tr("<html>Failed to initialize preferences.<br>Failed to reset preference file to default: {0}</html>",getPreferenceFile().getAbsoluteFile()),
902                    tr("Error"),
903                    JOptionPane.ERROR_MESSAGE
904            );
905            return;
906        }
907        try {
908            load();
909        } catch (Exception e) {
910            Main.error(e);
911            File backupFile = new File(prefDir,"preferences.xml.bak");
912            JOptionPane.showMessageDialog(
913                    Main.parent,
914                    tr("<html>Preferences file had errors.<br> Making backup of old one to <br>{0}<br> and creating a new default preference file.</html>", backupFile.getAbsoluteFile()),
915                    tr("Error"),
916                    JOptionPane.ERROR_MESSAGE
917            );
918            Main.platform.rename(preferenceFile, backupFile);
919            try {
920                resetToDefault();
921                save();
922            } catch(IOException e1) {
923                Main.error(e1);
924                Main.warn(tr("Failed to initialize preferences. Failed to reset preference file to default: {0}", getPreferenceFile()));
925            }
926        }
927    }
928
929    public final void resetToDefault(){
930        settingsMap.clear();
931    }
932
933    /**
934     * Convenience method for accessing colour preferences.
935     *
936     * @param colName name of the colour
937     * @param def default value
938     * @return a Color object for the configured colour, or the default value if none configured.
939     */
940    public synchronized Color getColor(String colName, Color def) {
941        return getColor(colName, null, def);
942    }
943
944    public synchronized Color getUIColor(String colName) {
945        return UIManager.getColor(colName);
946    }
947
948    /* only for preferences */
949    public synchronized String getColorName(String o) {
950        try {
951            Matcher m = Pattern.compile("mappaint\\.(.+?)\\.(.+)").matcher(o);
952            if (m.matches()) {
953                return tr("Paint style {0}: {1}", tr(I18n.escape(m.group(1))), tr(I18n.escape(m.group(2))));
954            }
955        } catch (Exception e) {
956            Main.warn(e);
957        }
958        try {
959            Matcher m = Pattern.compile("layer (.+)").matcher(o);
960            if (m.matches()) {
961                return tr("Layer: {0}", tr(I18n.escape(m.group(1))));
962            }
963        } catch (Exception e) {
964            Main.warn(e);
965        }
966        return tr(I18n.escape(colornames.containsKey(o) ? colornames.get(o) : o));
967    }
968
969    /**
970     * Returns the color for the given key.
971     * @param key The color key
972     * @return the color
973     */
974    public Color getColor(ColorKey key) {
975        return getColor(key.getColorName(), key.getSpecialName(), key.getDefaultValue());
976    }
977
978    /**
979     * Convenience method for accessing colour preferences.
980     *
981     * @param colName name of the colour
982     * @param specName name of the special colour settings
983     * @param def default value
984     * @return a Color object for the configured colour, or the default value if none configured.
985     */
986    public synchronized Color getColor(String colName, String specName, Color def) {
987        String colKey = ColorProperty.getColorKey(colName);
988        if(!colKey.equals(colName)) {
989            colornames.put(colKey, colName);
990        }
991        String colStr = specName != null ? get("color."+specName) : "";
992        if (colStr.isEmpty()) {
993            colStr = get("color." + colKey, ColorHelper.color2html(def, true));
994        }
995        if (colStr != null && !colStr.isEmpty()) {
996            return ColorHelper.html2color(colStr);
997        } else {
998            return def;
999        }
1000    }
1001
1002    public synchronized Color getDefaultColor(String colKey) {
1003        StringSetting col = Utils.cast(defaultsMap.get("color."+colKey), StringSetting.class);
1004        String colStr = col == null ? null : col.getValue();
1005        return colStr == null || colStr.isEmpty() ? null : ColorHelper.html2color(colStr);
1006    }
1007
1008    public synchronized boolean putColor(String colKey, Color val) {
1009        return put("color."+colKey, val != null ? ColorHelper.color2html(val, true) : null);
1010    }
1011
1012    public synchronized int getInteger(String key, int def) {
1013        String v = get(key, Integer.toString(def));
1014        if(v.isEmpty())
1015            return def;
1016
1017        try {
1018            return Integer.parseInt(v);
1019        } catch(NumberFormatException e) {
1020            // fall out
1021        }
1022        return def;
1023    }
1024
1025    public synchronized int getInteger(String key, String specName, int def) {
1026        String v = get(key+"."+specName);
1027        if(v.isEmpty())
1028            v = get(key,Integer.toString(def));
1029        if(v.isEmpty())
1030            return def;
1031
1032        try {
1033            return Integer.parseInt(v);
1034        } catch(NumberFormatException e) {
1035            // fall out
1036        }
1037        return def;
1038    }
1039
1040    public synchronized long getLong(String key, long def) {
1041        String v = get(key, Long.toString(def));
1042        if(null == v)
1043            return def;
1044
1045        try {
1046            return Long.parseLong(v);
1047        } catch(NumberFormatException e) {
1048            // fall out
1049        }
1050        return def;
1051    }
1052
1053    public synchronized double getDouble(String key, double def) {
1054        String v = get(key, Double.toString(def));
1055        if(null == v)
1056            return def;
1057
1058        try {
1059            return Double.parseDouble(v);
1060        } catch(NumberFormatException e) {
1061            // fall out
1062        }
1063        return def;
1064    }
1065
1066    /**
1067     * Get a list of values for a certain key
1068     * @param key the identifier for the setting
1069     * @param def the default value.
1070     * @return the corresponding value if the property has been set before,
1071     *  def otherwise
1072     */
1073    public Collection<String> getCollection(String key, Collection<String> def) {
1074        return getSetting(key, ListSetting.create(def), ListSetting.class).getValue();
1075    }
1076
1077    /**
1078     * Get a list of values for a certain key
1079     * @param key the identifier for the setting
1080     * @return the corresponding value if the property has been set before,
1081     *  an empty Collection otherwise.
1082     */
1083    public Collection<String> getCollection(String key) {
1084        Collection<String> val = getCollection(key, null);
1085        return val == null ? Collections.<String>emptyList() : val;
1086    }
1087
1088    public synchronized void removeFromCollection(String key, String value) {
1089        List<String> a = new ArrayList<>(getCollection(key, Collections.<String>emptyList()));
1090        a.remove(value);
1091        putCollection(key, a);
1092    }
1093
1094    /**
1095     * Set a value for a certain setting. The changed setting is saved
1096     * to the preference file immediately. Due to caching mechanisms on modern
1097     * operating systems and hardware, this shouldn't be a performance problem.
1098     * @param key the unique identifier for the setting
1099     * @param setting the value of the setting. In case it is null, the key-value
1100     * entry will be removed.
1101     * @return true, if something has changed (i.e. value is different than before)
1102     */
1103    public boolean putSetting(final String key, Setting<?> setting) {
1104        CheckParameterUtil.ensureParameterNotNull(key);
1105        if (setting != null && setting.getValue() == null)
1106            throw new IllegalArgumentException("setting argument must not have null value");
1107        Setting<?> settingOld;
1108        Setting<?> settingCopy = null;
1109        synchronized (this) {
1110            if (setting == null) {
1111                settingOld = settingsMap.remove(key);
1112                if (settingOld == null)
1113                    return false;
1114            } else {
1115                settingOld = settingsMap.get(key);
1116                if (setting.equals(settingOld))
1117                    return false;
1118                if (settingOld == null && setting.equals(defaultsMap.get(key)))
1119                    return false;
1120                settingCopy = setting.copy();
1121                settingsMap.put(key, settingCopy);
1122            }
1123            if (saveOnPut) {
1124                try {
1125                    save();
1126                } catch (IOException e){
1127                    Main.warn(tr("Failed to persist preferences to ''{0}''", getPreferenceFile().getAbsoluteFile()));
1128                }
1129            }
1130        }
1131        // Call outside of synchronized section in case some listener wait for other thread that wait for preference lock
1132        firePreferenceChanged(key, settingOld, settingCopy);
1133        return true;
1134    }
1135
1136    public synchronized Setting<?> getSetting(String key, Setting<?> def) {
1137        return getSetting(key, def, Setting.class);
1138    }
1139
1140    /**
1141     * Get settings value for a certain key and provide default a value.
1142     * @param <T> the setting type
1143     * @param key the identifier for the setting
1144     * @param def the default value. For each call of getSetting() with a given
1145     * key, the default value must be the same. <code>def</code> must not be
1146     * null, but the value of <code>def</code> can be null.
1147     * @param klass the setting type (same as T)
1148     * @return the corresponding value if the property has been set before,
1149     *  def otherwise
1150     */
1151    @SuppressWarnings("unchecked")
1152    public synchronized <T extends Setting<?>> T getSetting(String key, T def, Class<T> klass) {
1153        CheckParameterUtil.ensureParameterNotNull(key);
1154        CheckParameterUtil.ensureParameterNotNull(def);
1155        Setting<?> oldDef = defaultsMap.get(key);
1156        if (oldDef != null && oldDef.getValue() != null && def.getValue() != null && !def.equals(oldDef)) {
1157            Main.info("Defaults for " + key + " differ: " + def + " != " + defaultsMap.get(key));
1158        }
1159        if (def.getValue() != null || oldDef == null) {
1160            defaultsMap.put(key, def.copy());
1161        }
1162        Setting<?> prop = settingsMap.get(key);
1163        if (klass.isInstance(prop)) {
1164            return (T) prop;
1165        } else {
1166            return def;
1167        }
1168    }
1169
1170    public boolean putCollection(String key, Collection<String> value) {
1171        return putSetting(key, value == null ? null : ListSetting.create(value));
1172    }
1173
1174    /**
1175     * Saves at most {@code maxsize} items of collection {@code val}.
1176     */
1177    public boolean putCollectionBounded(String key, int maxsize, Collection<String> val) {
1178        Collection<String> newCollection = new ArrayList<>(Math.min(maxsize, val.size()));
1179        for (String i : val) {
1180            if (newCollection.size() >= maxsize) {
1181                break;
1182            }
1183            newCollection.add(i);
1184        }
1185        return putCollection(key, newCollection);
1186    }
1187
1188    /**
1189     * Used to read a 2-dimensional array of strings from the preference file.
1190     * If not a single entry could be found, <code>def</code> is returned.
1191     */
1192    @SuppressWarnings({ "unchecked", "rawtypes" })
1193    public synchronized Collection<Collection<String>> getArray(String key, Collection<Collection<String>> def) {
1194        ListListSetting val = getSetting(key, ListListSetting.create(def), ListListSetting.class);
1195        return (Collection) val.getValue();
1196    }
1197
1198    public Collection<Collection<String>> getArray(String key) {
1199        Collection<Collection<String>> res = getArray(key, null);
1200        return res == null ? Collections.<Collection<String>>emptyList() : res;
1201    }
1202
1203    public boolean putArray(String key, Collection<Collection<String>> value) {
1204        return putSetting(key, value == null ? null : ListListSetting.create(value));
1205    }
1206
1207    public Collection<Map<String, String>> getListOfStructs(String key, Collection<Map<String, String>> def) {
1208        return getSetting(key, new MapListSetting(def == null ? null : new ArrayList<>(def)), MapListSetting.class).getValue();
1209    }
1210
1211    public boolean putListOfStructs(String key, Collection<Map<String, String>> value) {
1212        return putSetting(key, value == null ? null : new MapListSetting(new ArrayList<>(value)));
1213    }
1214
1215    @Retention(RetentionPolicy.RUNTIME) public @interface pref { }
1216    @Retention(RetentionPolicy.RUNTIME) public @interface writeExplicitly { }
1217
1218    /**
1219     * Get a list of hashes which are represented by a struct-like class.
1220     * Possible properties are given by fields of the class klass that have
1221     * the @pref annotation.
1222     * Default constructor is used to initialize the struct objects, properties
1223     * then override some of these default values.
1224     * @param key main preference key
1225     * @param klass The struct class
1226     * @return a list of objects of type T or an empty list if nothing was found
1227     */
1228    public <T> List<T> getListOfStructs(String key, Class<T> klass) {
1229        List<T> r = getListOfStructs(key, null, klass);
1230        if (r == null)
1231            return Collections.emptyList();
1232        else
1233            return r;
1234    }
1235
1236    /**
1237     * same as above, but returns def if nothing was found
1238     */
1239    public <T> List<T> getListOfStructs(String key, Collection<T> def, Class<T> klass) {
1240        Collection<Map<String,String>> prop =
1241            getListOfStructs(key, def == null ? null : serializeListOfStructs(def, klass));
1242        if (prop == null)
1243            return def == null ? null : new ArrayList<>(def);
1244        List<T> lst = new ArrayList<>();
1245        for (Map<String,String> entries : prop) {
1246            T struct = deserializeStruct(entries, klass);
1247            lst.add(struct);
1248        }
1249        return lst;
1250    }
1251
1252    /**
1253     * Save a list of hashes represented by a struct-like class.
1254     * Considers only fields that have the @pref annotation.
1255     * In addition it does not write fields with null values. (Thus they are cleared)
1256     * Default values are given by the field values after default constructor has
1257     * been called.
1258     * Fields equal to the default value are not written unless the field has
1259     * the @writeExplicitly annotation.
1260     * @param key main preference key
1261     * @param val the list that is supposed to be saved
1262     * @param klass The struct class
1263     * @return true if something has changed
1264     */
1265    public <T> boolean putListOfStructs(String key, Collection<T> val, Class<T> klass) {
1266        return putListOfStructs(key, serializeListOfStructs(val, klass));
1267    }
1268
1269    private <T> Collection<Map<String,String>> serializeListOfStructs(Collection<T> l, Class<T> klass) {
1270        if (l == null)
1271            return null;
1272        Collection<Map<String,String>> vals = new ArrayList<>();
1273        for (T struct : l) {
1274            if (struct == null) {
1275                continue;
1276            }
1277            vals.add(serializeStruct(struct, klass));
1278        }
1279        return vals;
1280    }
1281
1282    public static <T> Map<String,String> serializeStruct(T struct, Class<T> klass) {
1283        T structPrototype;
1284        try {
1285            structPrototype = klass.newInstance();
1286        } catch (InstantiationException | IllegalAccessException ex) {
1287            throw new RuntimeException(ex);
1288        }
1289
1290        Map<String,String> hash = new LinkedHashMap<>();
1291        for (Field f : klass.getDeclaredFields()) {
1292            if (f.getAnnotation(pref.class) == null) {
1293                continue;
1294            }
1295            f.setAccessible(true);
1296            try {
1297                Object fieldValue = f.get(struct);
1298                Object defaultFieldValue = f.get(structPrototype);
1299                if (fieldValue != null) {
1300                    if (f.getAnnotation(writeExplicitly.class) != null || !Objects.equals(fieldValue, defaultFieldValue)) {
1301                        hash.put(f.getName().replace("_", "-"), fieldValue.toString());
1302                    }
1303                }
1304            } catch (IllegalArgumentException | IllegalAccessException ex) {
1305                throw new RuntimeException(ex);
1306            }
1307        }
1308        return hash;
1309    }
1310
1311    public static <T> T deserializeStruct(Map<String,String> hash, Class<T> klass) {
1312        T struct = null;
1313        try {
1314            struct = klass.newInstance();
1315        } catch (InstantiationException | IllegalAccessException ex) {
1316            throw new RuntimeException(ex);
1317        }
1318        for (Entry<String,String> key_value : hash.entrySet()) {
1319            Object value = null;
1320            Field f;
1321            try {
1322                f = klass.getDeclaredField(key_value.getKey().replace("-", "_"));
1323            } catch (NoSuchFieldException ex) {
1324                continue;
1325            } catch (SecurityException ex) {
1326                throw new RuntimeException(ex);
1327            }
1328            if (f.getAnnotation(pref.class) == null) {
1329                continue;
1330            }
1331            f.setAccessible(true);
1332            if (f.getType() == Boolean.class || f.getType() == boolean.class) {
1333                value = Boolean.parseBoolean(key_value.getValue());
1334            } else if (f.getType() == Integer.class || f.getType() == int.class) {
1335                try {
1336                    value = Integer.parseInt(key_value.getValue());
1337                } catch (NumberFormatException nfe) {
1338                    continue;
1339                }
1340            } else if (f.getType() == Double.class || f.getType() == double.class) {
1341                try {
1342                    value = Double.parseDouble(key_value.getValue());
1343                } catch (NumberFormatException nfe) {
1344                    continue;
1345                }
1346            } else  if (f.getType() == String.class) {
1347                value = key_value.getValue();
1348            } else
1349                throw new RuntimeException("unsupported preference primitive type");
1350
1351            try {
1352                f.set(struct, value);
1353            } catch (IllegalArgumentException ex) {
1354                throw new AssertionError(ex);
1355            } catch (IllegalAccessException ex) {
1356                throw new RuntimeException(ex);
1357            }
1358        }
1359        return struct;
1360    }
1361
1362    public Map<String, Setting<?>> getAllSettings() {
1363        return new TreeMap<>(settingsMap);
1364    }
1365
1366    public Map<String, Setting<?>> getAllDefaults() {
1367        return new TreeMap<>(defaultsMap);
1368    }
1369
1370    /**
1371     * Updates system properties with the current values in the preferences.
1372     *
1373     */
1374    public void updateSystemProperties() {
1375        if(getBoolean("prefer.ipv6", false)) {
1376            // never set this to false, only true!
1377            Utils.updateSystemProperty("java.net.preferIPv6Addresses", "true");
1378        }
1379        Utils.updateSystemProperty("http.agent", Version.getInstance().getAgentString());
1380        Utils.updateSystemProperty("user.language", get("language"));
1381        // Workaround to fix a Java bug.
1382        // Force AWT toolkit to update its internal preferences (fix #3645).
1383        // This ugly hack comes from Sun bug database: https://bugs.openjdk.java.net/browse/JDK-6292739
1384        try {
1385            Field field = Toolkit.class.getDeclaredField("resources");
1386            field.setAccessible(true);
1387            field.set(null, ResourceBundle.getBundle("sun.awt.resources.awt"));
1388        } catch (Exception e) {
1389            // Ignore all exceptions
1390        }
1391        // Workaround to fix a Java "feature"
1392        // See http://stackoverflow.com/q/7615645/2257172 and #9875
1393        if (getBoolean("jdk.tls.disableSNIExtension", true)) {
1394            Utils.updateSystemProperty("jsse.enableSNIExtension", "false");
1395        }
1396        // Workaround to fix another Java bug
1397        // Force Java 7 to use old sorting algorithm of Arrays.sort (fix #8712).
1398        // See Oracle bug database: https://bugs.openjdk.java.net/browse/JDK-7075600
1399        // and https://bugs.openjdk.java.net/browse/JDK-6923200
1400        if (getBoolean("jdk.Arrays.useLegacyMergeSort", !Version.getInstance().isLocalBuild())) {
1401            Utils.updateSystemProperty("java.util.Arrays.useLegacyMergeSort", "true");
1402        }
1403    }
1404
1405    /**
1406     * Replies the collection of plugin site URLs from where plugin lists can be downloaded.
1407     * @return the collection of plugin site URLs
1408     */
1409    public Collection<String> getPluginSites() {
1410        return getCollection("pluginmanager.sites", Collections.singleton(Main.getJOSMWebsite()+"/pluginicons%<?plugins=>"));
1411    }
1412
1413    /**
1414     * Sets the collection of plugin site URLs.
1415     *
1416     * @param sites the site URLs
1417     */
1418    public void setPluginSites(Collection<String> sites) {
1419        putCollection("pluginmanager.sites", sites);
1420    }
1421
1422    protected XMLStreamReader parser;
1423
1424    public void validateXML(Reader in) throws IOException, SAXException {
1425        SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
1426        try (InputStream xsdStream = new CachedFile("resource://data/preferences.xsd").getInputStream()) {
1427            Schema schema = factory.newSchema(new StreamSource(xsdStream));
1428            Validator validator = schema.newValidator();
1429            validator.validate(new StreamSource(in));
1430        }
1431    }
1432
1433    public void fromXML(Reader in) throws XMLStreamException {
1434        XMLStreamReader parser = XMLInputFactory.newInstance().createXMLStreamReader(in);
1435        this.parser = parser;
1436        parse();
1437    }
1438
1439    public void parse() throws XMLStreamException {
1440        int event = parser.getEventType();
1441        while (true) {
1442            if (event == XMLStreamConstants.START_ELEMENT) {
1443                parseRoot();
1444            } else if (event == XMLStreamConstants.END_ELEMENT) {
1445                return;
1446            }
1447            if (parser.hasNext()) {
1448                event = parser.next();
1449            } else {
1450                break;
1451            }
1452        }
1453        parser.close();
1454    }
1455
1456    public void parseRoot() throws XMLStreamException {
1457        while (true) {
1458            int event = parser.next();
1459            if (event == XMLStreamConstants.START_ELEMENT) {
1460                String localName = parser.getLocalName();
1461                switch(localName) {
1462                case "tag":
1463                    settingsMap.put(parser.getAttributeValue(null, "key"), new StringSetting(parser.getAttributeValue(null, "value")));
1464                    jumpToEnd();
1465                    break;
1466                case "list":
1467                case "collection":
1468                case "lists":
1469                case "maps":
1470                    parseToplevelList();
1471                    break;
1472                default:
1473                    throwException("Unexpected element: "+localName);
1474                }
1475            } else if (event == XMLStreamConstants.END_ELEMENT) {
1476                return;
1477            }
1478        }
1479    }
1480
1481    private void jumpToEnd() throws XMLStreamException {
1482        while (true) {
1483            int event = parser.next();
1484            if (event == XMLStreamConstants.START_ELEMENT) {
1485                jumpToEnd();
1486            } else if (event == XMLStreamConstants.END_ELEMENT) {
1487                return;
1488            }
1489        }
1490    }
1491
1492    protected void parseToplevelList() throws XMLStreamException {
1493        String key = parser.getAttributeValue(null, "key");
1494        String name = parser.getLocalName();
1495
1496        List<String> entries = null;
1497        List<List<String>> lists = null;
1498        List<Map<String, String>> maps = null;
1499        while (true) {
1500            int event = parser.next();
1501            if (event == XMLStreamConstants.START_ELEMENT) {
1502                String localName = parser.getLocalName();
1503                switch(localName) {
1504                case "entry":
1505                    if (entries == null) {
1506                        entries = new ArrayList<>();
1507                    }
1508                    entries.add(parser.getAttributeValue(null, "value"));
1509                    jumpToEnd();
1510                    break;
1511                case "list":
1512                    if (lists == null) {
1513                        lists = new ArrayList<>();
1514                    }
1515                    lists.add(parseInnerList());
1516                    break;
1517                case "map":
1518                    if (maps == null) {
1519                        maps = new ArrayList<>();
1520                    }
1521                    maps.add(parseMap());
1522                    break;
1523                default:
1524                    throwException("Unexpected element: "+localName);
1525                }
1526            } else if (event == XMLStreamConstants.END_ELEMENT) {
1527                break;
1528            }
1529        }
1530        if (entries != null) {
1531            settingsMap.put(key, new ListSetting(Collections.unmodifiableList(entries)));
1532        } else if (lists != null) {
1533            settingsMap.put(key, new ListListSetting(Collections.unmodifiableList(lists)));
1534        } else if (maps != null) {
1535            settingsMap.put(key, new MapListSetting(Collections.unmodifiableList(maps)));
1536        } else {
1537            if ("lists".equals(name)) {
1538                settingsMap.put(key, new ListListSetting(Collections.<List<String>>emptyList()));
1539            } else if ("maps".equals(name)) {
1540                settingsMap.put(key, new MapListSetting(Collections.<Map<String, String>>emptyList()));
1541            } else {
1542                settingsMap.put(key, new ListSetting(Collections.<String>emptyList()));
1543            }
1544        }
1545    }
1546
1547    protected List<String> parseInnerList() throws XMLStreamException {
1548        List<String> entries = new ArrayList<>();
1549        while (true) {
1550            int event = parser.next();
1551            if (event == XMLStreamConstants.START_ELEMENT) {
1552                if ("entry".equals(parser.getLocalName())) {
1553                    entries.add(parser.getAttributeValue(null, "value"));
1554                    jumpToEnd();
1555                } else {
1556                    throwException("Unexpected element: "+parser.getLocalName());
1557                }
1558            } else if (event == XMLStreamConstants.END_ELEMENT) {
1559                break;
1560            }
1561        }
1562        return Collections.unmodifiableList(entries);
1563    }
1564
1565    protected Map<String, String> parseMap() throws XMLStreamException {
1566        Map<String, String> map = new LinkedHashMap<>();
1567        while (true) {
1568            int event = parser.next();
1569            if (event == XMLStreamConstants.START_ELEMENT) {
1570                if ("tag".equals(parser.getLocalName())) {
1571                    map.put(parser.getAttributeValue(null, "key"), parser.getAttributeValue(null, "value"));
1572                    jumpToEnd();
1573                } else {
1574                    throwException("Unexpected element: "+parser.getLocalName());
1575                }
1576            } else if (event == XMLStreamConstants.END_ELEMENT) {
1577                break;
1578            }
1579        }
1580        return Collections.unmodifiableMap(map);
1581    }
1582
1583    protected void throwException(String msg) {
1584        throw new RuntimeException(msg + tr(" (at line {0}, column {1})", parser.getLocation().getLineNumber(), parser.getLocation().getColumnNumber()));
1585    }
1586
1587    private class SettingToXml implements SettingVisitor {
1588        private StringBuilder b;
1589        private boolean noPassword;
1590        private String key;
1591
1592        public SettingToXml(StringBuilder b, boolean noPassword) {
1593            this.b = b;
1594            this.noPassword = noPassword;
1595        }
1596
1597        public void setKey(String key) {
1598            this.key = key;
1599        }
1600
1601        @Override
1602        public void visit(StringSetting setting) {
1603            if (noPassword && "osm-server.password".equals(key))
1604                return; // do not store plain password.
1605            /* don't save default values */
1606            if (setting.equals(defaultsMap.get(key)))
1607                return;
1608            b.append("  <tag key='");
1609            b.append(XmlWriter.encode(key));
1610            b.append("' value='");
1611            b.append(XmlWriter.encode(setting.getValue()));
1612            b.append("'/>\n");
1613        }
1614
1615        @Override
1616        public void visit(ListSetting setting) {
1617            /* don't save default values */
1618            if (setting.equals(defaultsMap.get(key)))
1619                return;
1620            b.append("  <list key='").append(XmlWriter.encode(key)).append("'>\n");
1621            for (String s : setting.getValue()) {
1622                b.append("    <entry value='").append(XmlWriter.encode(s)).append("'/>\n");
1623            }
1624            b.append("  </list>\n");
1625        }
1626
1627        @Override
1628        public void visit(ListListSetting setting) {
1629            /* don't save default values */
1630            if (setting.equals(defaultsMap.get(key)))
1631                return;
1632            b.append("  <lists key='").append(XmlWriter.encode(key)).append("'>\n");
1633            for (List<String> list : setting.getValue()) {
1634                b.append("    <list>\n");
1635                for (String s : list) {
1636                    b.append("      <entry value='").append(XmlWriter.encode(s)).append("'/>\n");
1637                }
1638                b.append("    </list>\n");
1639            }
1640            b.append("  </lists>\n");
1641        }
1642
1643        @Override
1644        public void visit(MapListSetting setting) {
1645            b.append("  <maps key='").append(XmlWriter.encode(key)).append("'>\n");
1646            for (Map<String, String> struct : setting.getValue()) {
1647                b.append("    <map>\n");
1648                for (Entry<String, String> e : struct.entrySet()) {
1649                    b.append("      <tag key='").append(XmlWriter.encode(e.getKey())).append("' value='").append(XmlWriter.encode(e.getValue())).append("'/>\n");
1650                }
1651                b.append("    </map>\n");
1652            }
1653            b.append("  </maps>\n");
1654        }
1655    }
1656
1657    public String toXML(boolean nopass) {
1658        StringBuilder b = new StringBuilder(
1659                "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
1660                "<preferences xmlns=\""+Main.getXMLBase()+"/preferences-1.0\" version=\""+
1661                Version.getInstance().getVersion() + "\">\n");
1662        SettingToXml toXml = new SettingToXml(b, nopass);
1663        for (Entry<String, Setting<?>> e : settingsMap.entrySet()) {
1664            toXml.setKey(e.getKey());
1665            e.getValue().visit(toXml);
1666        }
1667        b.append("</preferences>\n");
1668        return b.toString();
1669    }
1670
1671    /**
1672     * Removes obsolete preference settings. If you throw out a once-used preference
1673     * setting, add it to the list here with an expiry date (written as comment). If you
1674     * see something with an expiry date in the past, remove it from the list.
1675     */
1676    public void removeObsolete() {
1677        /* update the data with old consumer key*/
1678        if(getInteger("josm.version", Version.getInstance().getVersion()) < 6076) {
1679            if(!get("oauth.access-token.key").isEmpty() && get("oauth.settings.consumer-key").isEmpty()) {
1680                put("oauth.settings.consumer-key", "AdCRxTpvnbmfV8aPqrTLyA");
1681                put("oauth.settings.consumer-secret", "XmYOiGY9hApytcBC3xCec3e28QBqOWz5g6DSb5UpE");
1682            }
1683        }
1684
1685        String[] obsolete = {
1686                "validator.tests",                             // 01/2014 - can be removed end-2014. Replaced by validator.skip
1687                "validator.testsBeforeUpload",                 // 01/2014 - can be removed end-2014. Replaced by validator.skipBeforeUpload
1688                "validator.TagChecker.sources",                // 01/2014 - can be removed end-2014. Replaced by validator.TagChecker.source
1689                "validator.TagChecker.usedatafile",            // 01/2014 - can be removed end-2014. Replaced by validator.TagChecker.source
1690                "validator.TagChecker.useignorefile",          // 01/2014 - can be removed end-2014. Replaced by validator.TagChecker.source
1691                "validator.TagChecker.usespellfile",           // 01/2014 - can be removed end-2014. Replaced by validator.TagChecker.source
1692                "validator.org.openstreetmap.josm.data.validation.tests.MapCSSTagChecker.sources" // 01/2014 - can be removed end-2014. Replaced by validator.org.openstreetmap.josm.data.validation.tests.MapCSSTagChecker.entries
1693        };
1694        for (String key : obsolete) {
1695            if (settingsMap.containsKey(key)) {
1696                settingsMap.remove(key);
1697                Main.info(tr("Preference setting {0} has been removed since it is no longer used.", key));
1698            }
1699        }
1700    }
1701
1702    /**
1703     * Enables or not the preferences file auto-save mechanism (save each time a setting is changed).
1704     * This behaviour is enabled by default.
1705     * @param enable if {@code true}, makes JOSM save preferences file each time a setting is changed
1706     * @since 7085
1707     */
1708    public final void enableSaveOnPut(boolean enable) {
1709        synchronized (this) {
1710            saveOnPut = enable;
1711        }
1712    }
1713}