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