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

Revision 4920, 65.9 KB checked in by stoecker, 2 hours ago (diff)

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