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