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

Last change on this file since 5761 was 5761, checked in by stoecker, 11 years ago

drop outdated configfile handling routines

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