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