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