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