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