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

Revision 5114, 67.8 KB checked in by akks, 2 months ago (diff)

see #4421: Mechanism to modify JOSM settings and store files, advanced preferences dialog modifications
+ JavaScript configuration API
+ Asynchronous file download task DownloadFileTask
+ Function to export arbitrary preference keys to file

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