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