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