001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data; 003 004import static org.openstreetmap.josm.tools.I18n.marktr; 005import static org.openstreetmap.josm.tools.I18n.tr; 006 007import java.awt.Color; 008import java.awt.GraphicsEnvironment; 009import java.awt.Toolkit; 010import java.io.File; 011import java.io.FileOutputStream; 012import java.io.IOException; 013import java.io.OutputStreamWriter; 014import java.io.PrintWriter; 015import java.io.Reader; 016import java.io.StringReader; 017import java.io.StringWriter; 018import java.lang.annotation.Retention; 019import java.lang.annotation.RetentionPolicy; 020import java.lang.reflect.Field; 021import java.nio.charset.StandardCharsets; 022import java.util.ArrayList; 023import java.util.Collection; 024import java.util.Collections; 025import java.util.HashMap; 026import java.util.HashSet; 027import java.util.Iterator; 028import java.util.LinkedHashMap; 029import java.util.LinkedList; 030import java.util.List; 031import java.util.Map; 032import java.util.Map.Entry; 033import java.util.MissingResourceException; 034import java.util.Objects; 035import java.util.ResourceBundle; 036import java.util.Set; 037import java.util.SortedMap; 038import java.util.TreeMap; 039import java.util.concurrent.CopyOnWriteArrayList; 040import java.util.regex.Matcher; 041import java.util.regex.Pattern; 042 043import javax.json.Json; 044import javax.json.JsonArray; 045import javax.json.JsonArrayBuilder; 046import javax.json.JsonObject; 047import javax.json.JsonObjectBuilder; 048import javax.json.JsonReader; 049import javax.json.JsonString; 050import javax.json.JsonValue; 051import javax.json.JsonWriter; 052import javax.swing.JOptionPane; 053import javax.xml.stream.XMLStreamException; 054 055import org.openstreetmap.josm.Main; 056import org.openstreetmap.josm.data.preferences.ColorProperty; 057import org.openstreetmap.josm.data.preferences.ListListSetting; 058import org.openstreetmap.josm.data.preferences.ListSetting; 059import org.openstreetmap.josm.data.preferences.MapListSetting; 060import org.openstreetmap.josm.data.preferences.PreferencesReader; 061import org.openstreetmap.josm.data.preferences.PreferencesWriter; 062import org.openstreetmap.josm.data.preferences.Setting; 063import org.openstreetmap.josm.data.preferences.StringSetting; 064import org.openstreetmap.josm.io.OfflineAccessException; 065import org.openstreetmap.josm.io.OnlineResource; 066import org.openstreetmap.josm.tools.CheckParameterUtil; 067import org.openstreetmap.josm.tools.ColorHelper; 068import org.openstreetmap.josm.tools.FilteredCollection; 069import org.openstreetmap.josm.tools.I18n; 070import org.openstreetmap.josm.tools.MultiMap; 071import org.openstreetmap.josm.tools.Predicate; 072import org.openstreetmap.josm.tools.Utils; 073import org.xml.sax.SAXException; 074 075/** 076 * This class holds all preferences for JOSM. 077 * 078 * Other classes can register their beloved properties here. All properties will be 079 * saved upon set-access. 080 * 081 * Each property is a key=setting pair, where key is a String and setting can be one of 082 * 4 types: 083 * string, list, list of lists and list of maps. 084 * In addition, each key has a unique default value that is set when the value is first 085 * accessed using one of the get...() methods. You can use the same preference 086 * key in different parts of the code, but the default value must be the same 087 * everywhere. A default value of null means, the setting has been requested, but 088 * no default value was set. This is used in advanced preferences to present a list 089 * off all possible settings. 090 * 091 * At the moment, you cannot put the empty string for string properties. 092 * put(key, "") means, the property is removed. 093 * 094 * @author imi 095 * @since 74 096 */ 097public class Preferences { 098 099 private static final String[] OBSOLETE_PREF_KEYS = { 100 }; 101 102 private static final long MAX_AGE_DEFAULT_PREFERENCES = 60L * 60L * 24L * 50L; // 50 days (in seconds) 103 104 /** 105 * Internal storage for the preference directory. 106 * Do not access this variable directly! 107 * @see #getPreferencesDirectory() 108 */ 109 private File preferencesDir; 110 111 /** 112 * Internal storage for the cache directory. 113 */ 114 private File cacheDir; 115 116 /** 117 * Internal storage for the user data directory. 118 */ 119 private File userdataDir; 120 121 /** 122 * Determines if preferences file is saved each time a property is changed. 123 */ 124 private boolean saveOnPut = true; 125 126 /** 127 * Maps the setting name to the current value of the setting. 128 * The map must not contain null as key or value. The mapped setting objects 129 * must not have a null value. 130 */ 131 protected final SortedMap<String, Setting<?>> settingsMap = new TreeMap<>(); 132 133 /** 134 * Maps the setting name to the default value of the setting. 135 * The map must not contain null as key or value. The value of the mapped 136 * setting objects can be null. 137 */ 138 protected final SortedMap<String, Setting<?>> defaultsMap = new TreeMap<>(); 139 140 private final Predicate<Entry<String, Setting<?>>> NO_DEFAULT_SETTINGS_ENTRY = new Predicate<Entry<String, Setting<?>>>() { 141 @Override 142 public boolean evaluate(Entry<String, Setting<?>> e) { 143 return !e.getValue().equals(defaultsMap.get(e.getKey())); 144 } 145 }; 146 147 /** 148 * Maps color keys to human readable color name 149 */ 150 protected final SortedMap<String, String> colornames = new TreeMap<>(); 151 152 /** 153 * Indicates whether {@link #init(boolean)} completed successfully. 154 * Used to decide whether to write backup preference file in {@link #save()} 155 */ 156 protected boolean initSuccessful; 157 158 /** 159 * Event triggered when a preference entry value changes. 160 */ 161 public interface PreferenceChangeEvent { 162 /** 163 * Returns the preference key. 164 * @return the preference key 165 */ 166 String getKey(); 167 168 /** 169 * Returns the old preference value. 170 * @return the old preference value 171 */ 172 Setting<?> getOldValue(); 173 174 /** 175 * Returns the new preference value. 176 * @return the new preference value 177 */ 178 Setting<?> getNewValue(); 179 } 180 181 /** 182 * Listener to preference change events. 183 */ 184 public interface PreferenceChangedListener { 185 /** 186 * Trigerred when a preference entry value changes. 187 * @param e the preference change event 188 */ 189 void preferenceChanged(PreferenceChangeEvent e); 190 } 191 192 private static class DefaultPreferenceChangeEvent implements PreferenceChangeEvent { 193 private final String key; 194 private final Setting<?> oldValue; 195 private final Setting<?> newValue; 196 197 DefaultPreferenceChangeEvent(String key, Setting<?> oldValue, Setting<?> newValue) { 198 this.key = key; 199 this.oldValue = oldValue; 200 this.newValue = newValue; 201 } 202 203 @Override 204 public String getKey() { 205 return key; 206 } 207 208 @Override 209 public Setting<?> getOldValue() { 210 return oldValue; 211 } 212 213 @Override 214 public Setting<?> getNewValue() { 215 return newValue; 216 } 217 } 218 219 public interface ColorKey { 220 String getColorName(); 221 222 String getSpecialName(); 223 224 Color getDefaultValue(); 225 } 226 227 private final CopyOnWriteArrayList<PreferenceChangedListener> listeners = new CopyOnWriteArrayList<>(); 228 229 /** 230 * Adds a new preferences listener. 231 * @param listener The listener to add 232 */ 233 public void addPreferenceChangeListener(PreferenceChangedListener listener) { 234 if (listener != null) { 235 listeners.addIfAbsent(listener); 236 } 237 } 238 239 /** 240 * Removes a preferences listener. 241 * @param listener The listener to remove 242 */ 243 public void removePreferenceChangeListener(PreferenceChangedListener listener) { 244 listeners.remove(listener); 245 } 246 247 protected void firePreferenceChanged(String key, Setting<?> oldValue, Setting<?> newValue) { 248 PreferenceChangeEvent evt = new DefaultPreferenceChangeEvent(key, oldValue, newValue); 249 for (PreferenceChangedListener l : listeners) { 250 l.preferenceChanged(evt); 251 } 252 } 253 254 /** 255 * Returns the user defined preferences directory, containing the preferences.xml file 256 * @return The user defined preferences directory, containing the preferences.xml file 257 * @since 7834 258 */ 259 public File getPreferencesDirectory() { 260 if (preferencesDir != null) 261 return preferencesDir; 262 String path; 263 path = System.getProperty("josm.pref"); 264 if (path != null) { 265 preferencesDir = new File(path).getAbsoluteFile(); 266 } else { 267 path = System.getProperty("josm.home"); 268 if (path != null) { 269 preferencesDir = new File(path).getAbsoluteFile(); 270 } else { 271 preferencesDir = Main.platform.getDefaultPrefDirectory(); 272 } 273 } 274 return preferencesDir; 275 } 276 277 /** 278 * Returns the user data directory, containing autosave, plugins, etc. 279 * Depending on the OS it may be the same directory as preferences directory. 280 * @return The user data directory, containing autosave, plugins, etc. 281 * @since 7834 282 */ 283 public File getUserDataDirectory() { 284 if (userdataDir != null) 285 return userdataDir; 286 String path; 287 path = System.getProperty("josm.userdata"); 288 if (path != null) { 289 userdataDir = new File(path).getAbsoluteFile(); 290 } else { 291 path = System.getProperty("josm.home"); 292 if (path != null) { 293 userdataDir = new File(path).getAbsoluteFile(); 294 } else { 295 userdataDir = Main.platform.getDefaultUserDataDirectory(); 296 } 297 } 298 return userdataDir; 299 } 300 301 /** 302 * Returns the user preferences file (preferences.xml). 303 * @return The user preferences file (preferences.xml) 304 */ 305 public File getPreferenceFile() { 306 return new File(getPreferencesDirectory(), "preferences.xml"); 307 } 308 309 /** 310 * Returns the cache file for default preferences. 311 * @return the cache file for default preferences 312 */ 313 public File getDefaultsCacheFile() { 314 return new File(getCacheDirectory(), "default_preferences.xml"); 315 } 316 317 /** 318 * Returns the user plugin directory. 319 * @return The user plugin directory 320 */ 321 public File getPluginsDirectory() { 322 return new File(getUserDataDirectory(), "plugins"); 323 } 324 325 /** 326 * Get the directory where cached content of any kind should be stored. 327 * 328 * If the directory doesn't exist on the file system, it will be created by this method. 329 * 330 * @return the cache directory 331 */ 332 public File getCacheDirectory() { 333 if (cacheDir != null) 334 return cacheDir; 335 String path = System.getProperty("josm.cache"); 336 if (path != null) { 337 cacheDir = new File(path).getAbsoluteFile(); 338 } else { 339 path = System.getProperty("josm.home"); 340 if (path != null) { 341 cacheDir = new File(path, "cache"); 342 } else { 343 path = get("cache.folder", null); 344 if (path != null) { 345 cacheDir = new File(path).getAbsoluteFile(); 346 } else { 347 cacheDir = Main.platform.getDefaultCacheDirectory(); 348 } 349 } 350 } 351 if (!cacheDir.exists() && !cacheDir.mkdirs()) { 352 Main.warn(tr("Failed to create missing cache directory: {0}", cacheDir.getAbsoluteFile())); 353 JOptionPane.showMessageDialog( 354 Main.parent, 355 tr("<html>Failed to create missing cache directory: {0}</html>", cacheDir.getAbsoluteFile()), 356 tr("Error"), 357 JOptionPane.ERROR_MESSAGE 358 ); 359 } 360 return cacheDir; 361 } 362 363 private static void addPossibleResourceDir(Set<String> locations, String s) { 364 if (s != null) { 365 if (!s.endsWith(File.separator)) { 366 s += File.separator; 367 } 368 locations.add(s); 369 } 370 } 371 372 /** 373 * Returns a set of all existing directories where resources could be stored. 374 * @return A set of all existing directories where resources could be stored. 375 */ 376 public Collection<String> getAllPossiblePreferenceDirs() { 377 Set<String> locations = new HashSet<>(); 378 addPossibleResourceDir(locations, getPreferencesDirectory().getPath()); 379 addPossibleResourceDir(locations, getUserDataDirectory().getPath()); 380 addPossibleResourceDir(locations, System.getenv("JOSM_RESOURCES")); 381 addPossibleResourceDir(locations, System.getProperty("josm.resources")); 382 if (Main.isPlatformWindows()) { 383 String appdata = System.getenv("APPDATA"); 384 if (System.getenv("ALLUSERSPROFILE") != null && appdata != null 385 && appdata.lastIndexOf(File.separator) != -1) { 386 appdata = appdata.substring(appdata.lastIndexOf(File.separator)); 387 locations.add(new File(new File(System.getenv("ALLUSERSPROFILE"), 388 appdata), "JOSM").getPath()); 389 } 390 } else { 391 locations.add("/usr/local/share/josm/"); 392 locations.add("/usr/local/lib/josm/"); 393 locations.add("/usr/share/josm/"); 394 locations.add("/usr/lib/josm/"); 395 } 396 return locations; 397 } 398 399 /** 400 * Get settings value for a certain key. 401 * @param key the identifier for the setting 402 * @return "" if there is nothing set for the preference key, the corresponding value otherwise. The result is not null. 403 */ 404 public synchronized String get(final String key) { 405 String value = get(key, null); 406 return value == null ? "" : value; 407 } 408 409 /** 410 * Get settings value for a certain key and provide default a value. 411 * @param key the identifier for the setting 412 * @param def the default value. For each call of get() with a given key, the default value must be the same. 413 * @return the corresponding value if the property has been set before, {@code def} otherwise 414 */ 415 public synchronized String get(final String key, final String def) { 416 return getSetting(key, new StringSetting(def), StringSetting.class).getValue(); 417 } 418 419 public synchronized Map<String, String> getAllPrefix(final String prefix) { 420 final Map<String, String> all = new TreeMap<>(); 421 for (final Entry<String, Setting<?>> e : settingsMap.entrySet()) { 422 if (e.getKey().startsWith(prefix) && (e.getValue() instanceof StringSetting)) { 423 all.put(e.getKey(), ((StringSetting) e.getValue()).getValue()); 424 } 425 } 426 return all; 427 } 428 429 public synchronized List<String> getAllPrefixCollectionKeys(final String prefix) { 430 final List<String> all = new LinkedList<>(); 431 for (Map.Entry<String, Setting<?>> entry : settingsMap.entrySet()) { 432 if (entry.getKey().startsWith(prefix) && entry.getValue() instanceof ListSetting) { 433 all.add(entry.getKey()); 434 } 435 } 436 return all; 437 } 438 439 public synchronized Map<String, String> getAllColors() { 440 final Map<String, String> all = new TreeMap<>(); 441 for (final Entry<String, Setting<?>> e : defaultsMap.entrySet()) { 442 if (e.getKey().startsWith("color.") && e.getValue() instanceof StringSetting) { 443 StringSetting d = (StringSetting) e.getValue(); 444 if (d.getValue() != null) { 445 all.put(e.getKey().substring(6), d.getValue()); 446 } 447 } 448 } 449 for (final Entry<String, Setting<?>> e : settingsMap.entrySet()) { 450 if (e.getKey().startsWith("color.") && (e.getValue() instanceof StringSetting)) { 451 all.put(e.getKey().substring(6), ((StringSetting) e.getValue()).getValue()); 452 } 453 } 454 return all; 455 } 456 457 public synchronized boolean getBoolean(final String key) { 458 String s = get(key, null); 459 return s != null && Boolean.parseBoolean(s); 460 } 461 462 public synchronized boolean getBoolean(final String key, final boolean def) { 463 return Boolean.parseBoolean(get(key, Boolean.toString(def))); 464 } 465 466 public synchronized boolean getBoolean(final String key, final String specName, final boolean def) { 467 boolean generic = getBoolean(key, def); 468 String skey = key+'.'+specName; 469 Setting<?> prop = settingsMap.get(skey); 470 if (prop instanceof StringSetting) 471 return Boolean.parseBoolean(((StringSetting) prop).getValue()); 472 else 473 return generic; 474 } 475 476 /** 477 * Set a value for a certain setting. 478 * @param key the unique identifier for the setting 479 * @param value the value of the setting. Can be null or "" which both removes the key-value entry. 480 * @return {@code true}, if something has changed (i.e. value is different than before) 481 */ 482 public boolean put(final String key, String value) { 483 if (value != null && value.isEmpty()) { 484 value = null; 485 } 486 return putSetting(key, value == null ? null : new StringSetting(value)); 487 } 488 489 public boolean put(final String key, final boolean value) { 490 return put(key, Boolean.toString(value)); 491 } 492 493 public boolean putInteger(final String key, final Integer value) { 494 return put(key, Integer.toString(value)); 495 } 496 497 public boolean putDouble(final String key, final Double value) { 498 return put(key, Double.toString(value)); 499 } 500 501 public boolean putLong(final String key, final Long value) { 502 return put(key, Long.toString(value)); 503 } 504 505 /** 506 * Called after every put. In case of a problem, do nothing but output the error in log. 507 * @throws IOException if any I/O error occurs 508 */ 509 public synchronized void save() throws IOException { 510 save(getPreferenceFile(), 511 new FilteredCollection<>(settingsMap.entrySet(), NO_DEFAULT_SETTINGS_ENTRY), false); 512 } 513 514 public synchronized void saveDefaults() throws IOException { 515 save(getDefaultsCacheFile(), defaultsMap.entrySet(), true); 516 } 517 518 protected void save(File prefFile, Collection<Entry<String, Setting<?>>> settings, boolean defaults) throws IOException { 519 520 if (!defaults) { 521 /* currently unused, but may help to fix configuration issues in future */ 522 putInteger("josm.version", Version.getInstance().getVersion()); 523 524 updateSystemProperties(); 525 } 526 527 File backupFile = new File(prefFile + "_backup"); 528 529 // Backup old preferences if there are old preferences 530 if (prefFile.exists() && prefFile.length() > 0 && initSuccessful) { 531 Utils.copyFile(prefFile, backupFile); 532 } 533 534 try (PrintWriter out = new PrintWriter(new OutputStreamWriter( 535 new FileOutputStream(prefFile + "_tmp"), StandardCharsets.UTF_8), false)) { 536 PreferencesWriter writer = new PreferencesWriter(out, false, defaults); 537 writer.write(settings); 538 } 539 540 File tmpFile = new File(prefFile + "_tmp"); 541 Utils.copyFile(tmpFile, prefFile); 542 Utils.deleteFile(tmpFile, marktr("Unable to delete temporary file {0}")); 543 544 setCorrectPermissions(prefFile); 545 setCorrectPermissions(backupFile); 546 } 547 548 private static void setCorrectPermissions(File file) { 549 if (!file.setReadable(false, false) && Main.isDebugEnabled()) { 550 Main.debug(tr("Unable to set file non-readable {0}", file.getAbsolutePath())); 551 } 552 if (!file.setWritable(false, false) && Main.isDebugEnabled()) { 553 Main.debug(tr("Unable to set file non-writable {0}", file.getAbsolutePath())); 554 } 555 if (!file.setExecutable(false, false) && Main.isDebugEnabled()) { 556 Main.debug(tr("Unable to set file non-executable {0}", file.getAbsolutePath())); 557 } 558 if (!file.setReadable(true, true) && Main.isDebugEnabled()) { 559 Main.debug(tr("Unable to set file readable {0}", file.getAbsolutePath())); 560 } 561 if (!file.setWritable(true, true) && Main.isDebugEnabled()) { 562 Main.debug(tr("Unable to set file writable {0}", file.getAbsolutePath())); 563 } 564 } 565 566 /** 567 * Loads preferences from settings file. 568 * @throws IOException if any I/O error occurs while reading the file 569 * @throws SAXException if the settings file does not contain valid XML 570 * @throws XMLStreamException if an XML error occurs while parsing the file (after validation) 571 */ 572 protected void load() throws IOException, SAXException, XMLStreamException { 573 File pref = getPreferenceFile(); 574 PreferencesReader.validateXML(pref); 575 PreferencesReader reader = new PreferencesReader(pref, false); 576 reader.parse(); 577 settingsMap.clear(); 578 settingsMap.putAll(reader.getSettings()); 579 updateSystemProperties(); 580 removeObsolete(reader.getVersion()); 581 } 582 583 /** 584 * Loads default preferences from default settings cache file. 585 * 586 * Discards entries older than {@link #MAX_AGE_DEFAULT_PREFERENCES}. 587 * 588 * @throws IOException if any I/O error occurs while reading the file 589 * @throws SAXException if the settings file does not contain valid XML 590 * @throws XMLStreamException if an XML error occurs while parsing the file (after validation) 591 */ 592 protected void loadDefaults() throws IOException, XMLStreamException, SAXException { 593 File def = getDefaultsCacheFile(); 594 PreferencesReader.validateXML(def); 595 PreferencesReader reader = new PreferencesReader(def, true); 596 reader.parse(); 597 defaultsMap.clear(); 598 long minTime = System.currentTimeMillis() / 1000 - MAX_AGE_DEFAULT_PREFERENCES; 599 for (Entry<String, Setting<?>> e : reader.getSettings().entrySet()) { 600 if (e.getValue().getTime() >= minTime) { 601 defaultsMap.put(e.getKey(), e.getValue()); 602 } 603 } 604 } 605 606 /** 607 * Loads preferences from XML reader. 608 * @param in XML reader 609 * @throws XMLStreamException if any XML stream error occurs 610 * @throws IOException if any I/O error occurs 611 */ 612 public void fromXML(Reader in) throws XMLStreamException, IOException { 613 PreferencesReader reader = new PreferencesReader(in, false); 614 reader.parse(); 615 settingsMap.clear(); 616 settingsMap.putAll(reader.getSettings()); 617 } 618 619 /** 620 * Initializes preferences. 621 * @param reset if {@code true}, current settings file is replaced by the default one 622 */ 623 public void init(boolean reset) { 624 initSuccessful = false; 625 // get the preferences. 626 File prefDir = getPreferencesDirectory(); 627 if (prefDir.exists()) { 628 if (!prefDir.isDirectory()) { 629 Main.warn(tr("Failed to initialize preferences. Preference directory ''{0}'' is not a directory.", 630 prefDir.getAbsoluteFile())); 631 JOptionPane.showMessageDialog( 632 Main.parent, 633 tr("<html>Failed to initialize preferences.<br>Preference directory ''{0}'' is not a directory.</html>", 634 prefDir.getAbsoluteFile()), 635 tr("Error"), 636 JOptionPane.ERROR_MESSAGE 637 ); 638 return; 639 } 640 } else { 641 if (!prefDir.mkdirs()) { 642 Main.warn(tr("Failed to initialize preferences. Failed to create missing preference directory: {0}", 643 prefDir.getAbsoluteFile())); 644 JOptionPane.showMessageDialog( 645 Main.parent, 646 tr("<html>Failed to initialize preferences.<br>Failed to create missing preference directory: {0}</html>", 647 prefDir.getAbsoluteFile()), 648 tr("Error"), 649 JOptionPane.ERROR_MESSAGE 650 ); 651 return; 652 } 653 } 654 655 File preferenceFile = getPreferenceFile(); 656 try { 657 if (!preferenceFile.exists()) { 658 Main.info(tr("Missing preference file ''{0}''. Creating a default preference file.", preferenceFile.getAbsoluteFile())); 659 resetToDefault(); 660 save(); 661 } else if (reset) { 662 File backupFile = new File(prefDir, "preferences.xml.bak"); 663 Main.platform.rename(preferenceFile, backupFile); 664 Main.warn(tr("Replacing existing preference file ''{0}'' with default preference file.", preferenceFile.getAbsoluteFile())); 665 resetToDefault(); 666 save(); 667 } 668 } catch (IOException e) { 669 Main.error(e); 670 JOptionPane.showMessageDialog( 671 Main.parent, 672 tr("<html>Failed to initialize preferences.<br>Failed to reset preference file to default: {0}</html>", 673 getPreferenceFile().getAbsoluteFile()), 674 tr("Error"), 675 JOptionPane.ERROR_MESSAGE 676 ); 677 return; 678 } 679 try { 680 load(); 681 initSuccessful = true; 682 } catch (IOException | SAXException | XMLStreamException e) { 683 Main.error(e); 684 File backupFile = new File(prefDir, "preferences.xml.bak"); 685 JOptionPane.showMessageDialog( 686 Main.parent, 687 tr("<html>Preferences file had errors.<br> Making backup of old one to <br>{0}<br> " + 688 "and creating a new default preference file.</html>", 689 backupFile.getAbsoluteFile()), 690 tr("Error"), 691 JOptionPane.ERROR_MESSAGE 692 ); 693 Main.platform.rename(preferenceFile, backupFile); 694 try { 695 resetToDefault(); 696 save(); 697 } catch (IOException e1) { 698 Main.error(e1); 699 Main.warn(tr("Failed to initialize preferences. Failed to reset preference file to default: {0}", getPreferenceFile())); 700 } 701 } 702 File def = getDefaultsCacheFile(); 703 if (def.exists()) { 704 try { 705 loadDefaults(); 706 } catch (IOException | XMLStreamException | SAXException e) { 707 Main.error(e); 708 Main.warn(tr("Failed to load defaults cache file: {0}", def)); 709 defaultsMap.clear(); 710 if (!def.delete()) { 711 Main.warn(tr("Failed to delete faulty defaults cache file: {0}", def)); 712 } 713 } 714 } 715 } 716 717 public final void resetToDefault() { 718 settingsMap.clear(); 719 } 720 721 /** 722 * Convenience method for accessing colour preferences. 723 * 724 * @param colName name of the colour 725 * @param def default value 726 * @return a Color object for the configured colour, or the default value if none configured. 727 */ 728 public synchronized Color getColor(String colName, Color def) { 729 return getColor(colName, null, def); 730 } 731 732 /* only for preferences */ 733 public synchronized String getColorName(String o) { 734 Matcher m = Pattern.compile("mappaint\\.(.+?)\\.(.+)").matcher(o); 735 if (m.matches()) { 736 return tr("Paint style {0}: {1}", tr(I18n.escape(m.group(1))), tr(I18n.escape(m.group(2)))); 737 } 738 m = Pattern.compile("layer (.+)").matcher(o); 739 if (m.matches()) { 740 return tr("Layer: {0}", tr(I18n.escape(m.group(1)))); 741 } 742 return tr(I18n.escape(colornames.containsKey(o) ? colornames.get(o) : o)); 743 } 744 745 /** 746 * Returns the color for the given key. 747 * @param key The color key 748 * @return the color 749 */ 750 public Color getColor(ColorKey key) { 751 return getColor(key.getColorName(), key.getSpecialName(), key.getDefaultValue()); 752 } 753 754 /** 755 * Convenience method for accessing colour preferences. 756 * 757 * @param colName name of the colour 758 * @param specName name of the special colour settings 759 * @param def default value 760 * @return a Color object for the configured colour, or the default value if none configured. 761 */ 762 public synchronized Color getColor(String colName, String specName, Color def) { 763 String colKey = ColorProperty.getColorKey(colName); 764 if (!colKey.equals(colName)) { 765 colornames.put(colKey, colName); 766 } 767 String colStr = specName != null ? get("color."+specName) : ""; 768 if (colStr.isEmpty()) { 769 colStr = get("color." + colKey, ColorHelper.color2html(def, true)); 770 } 771 if (colStr != null && !colStr.isEmpty()) { 772 return ColorHelper.html2color(colStr); 773 } else { 774 return def; 775 } 776 } 777 778 public synchronized Color getDefaultColor(String colKey) { 779 StringSetting col = Utils.cast(defaultsMap.get("color."+colKey), StringSetting.class); 780 String colStr = col == null ? null : col.getValue(); 781 return colStr == null || colStr.isEmpty() ? null : ColorHelper.html2color(colStr); 782 } 783 784 public synchronized boolean putColor(String colKey, Color val) { 785 return put("color."+colKey, val != null ? ColorHelper.color2html(val, true) : null); 786 } 787 788 public synchronized int getInteger(String key, int def) { 789 String v = get(key, Integer.toString(def)); 790 if (v.isEmpty()) 791 return def; 792 793 try { 794 return Integer.parseInt(v); 795 } catch (NumberFormatException e) { 796 // fall out 797 if (Main.isTraceEnabled()) { 798 Main.trace(e.getMessage()); 799 } 800 } 801 return def; 802 } 803 804 public synchronized int getInteger(String key, String specName, int def) { 805 String v = get(key+'.'+specName); 806 if (v.isEmpty()) 807 v = get(key, Integer.toString(def)); 808 if (v.isEmpty()) 809 return def; 810 811 try { 812 return Integer.parseInt(v); 813 } catch (NumberFormatException e) { 814 // fall out 815 if (Main.isTraceEnabled()) { 816 Main.trace(e.getMessage()); 817 } 818 } 819 return def; 820 } 821 822 public synchronized long getLong(String key, long def) { 823 String v = get(key, Long.toString(def)); 824 if (null == v) 825 return def; 826 827 try { 828 return Long.parseLong(v); 829 } catch (NumberFormatException e) { 830 // fall out 831 if (Main.isTraceEnabled()) { 832 Main.trace(e.getMessage()); 833 } 834 } 835 return def; 836 } 837 838 public synchronized double getDouble(String key, double def) { 839 String v = get(key, Double.toString(def)); 840 if (null == v) 841 return def; 842 843 try { 844 return Double.parseDouble(v); 845 } catch (NumberFormatException e) { 846 // fall out 847 if (Main.isTraceEnabled()) { 848 Main.trace(e.getMessage()); 849 } 850 } 851 return def; 852 } 853 854 /** 855 * Get a list of values for a certain key 856 * @param key the identifier for the setting 857 * @param def the default value. 858 * @return the corresponding value if the property has been set before, {@code def} otherwise 859 */ 860 public Collection<String> getCollection(String key, Collection<String> def) { 861 return getSetting(key, ListSetting.create(def), ListSetting.class).getValue(); 862 } 863 864 /** 865 * Get a list of values for a certain key 866 * @param key the identifier for the setting 867 * @return the corresponding value if the property has been set before, an empty collection otherwise. 868 */ 869 public Collection<String> getCollection(String key) { 870 Collection<String> val = getCollection(key, null); 871 return val == null ? Collections.<String>emptyList() : val; 872 } 873 874 public synchronized void removeFromCollection(String key, String value) { 875 List<String> a = new ArrayList<>(getCollection(key, Collections.<String>emptyList())); 876 a.remove(value); 877 putCollection(key, a); 878 } 879 880 /** 881 * Set a value for a certain setting. The changed setting is saved to the preference file immediately. 882 * Due to caching mechanisms on modern operating systems and hardware, this shouldn't be a performance problem. 883 * @param key the unique identifier for the setting 884 * @param setting the value of the setting. In case it is null, the key-value entry will be removed. 885 * @return {@code true}, if something has changed (i.e. value is different than before) 886 */ 887 public boolean putSetting(final String key, Setting<?> setting) { 888 CheckParameterUtil.ensureParameterNotNull(key); 889 if (setting != null && setting.getValue() == null) 890 throw new IllegalArgumentException("setting argument must not have null value"); 891 Setting<?> settingOld; 892 Setting<?> settingCopy = null; 893 synchronized (this) { 894 if (setting == null) { 895 settingOld = settingsMap.remove(key); 896 if (settingOld == null) 897 return false; 898 } else { 899 settingOld = settingsMap.get(key); 900 if (setting.equals(settingOld)) 901 return false; 902 if (settingOld == null && setting.equals(defaultsMap.get(key))) 903 return false; 904 settingCopy = setting.copy(); 905 settingsMap.put(key, settingCopy); 906 } 907 if (saveOnPut) { 908 try { 909 save(); 910 } catch (IOException e) { 911 Main.warn(e, tr("Failed to persist preferences to ''{0}''", getPreferenceFile().getAbsoluteFile())); 912 } 913 } 914 } 915 // Call outside of synchronized section in case some listener wait for other thread that wait for preference lock 916 firePreferenceChanged(key, settingOld, settingCopy); 917 return true; 918 } 919 920 public synchronized Setting<?> getSetting(String key, Setting<?> def) { 921 return getSetting(key, def, Setting.class); 922 } 923 924 /** 925 * Get settings value for a certain key and provide default a value. 926 * @param <T> the setting type 927 * @param key the identifier for the setting 928 * @param def the default value. For each call of getSetting() with a given key, the default value must be the same. 929 * <code>def</code> must not be null, but the value of <code>def</code> can be null. 930 * @param klass the setting type (same as T) 931 * @return the corresponding value if the property has been set before, {@code def} otherwise 932 */ 933 @SuppressWarnings("unchecked") 934 public synchronized <T extends Setting<?>> T getSetting(String key, T def, Class<T> klass) { 935 CheckParameterUtil.ensureParameterNotNull(key); 936 CheckParameterUtil.ensureParameterNotNull(def); 937 Setting<?> oldDef = defaultsMap.get(key); 938 if (oldDef != null && oldDef.isNew() && oldDef.getValue() != null && def.getValue() != null && !def.equals(oldDef)) { 939 Main.info("Defaults for " + key + " differ: " + def + " != " + defaultsMap.get(key)); 940 } 941 if (def.getValue() != null || oldDef == null) { 942 Setting<?> defCopy = def.copy(); 943 defCopy.setTime(System.currentTimeMillis() / 1000); 944 defCopy.setNew(true); 945 defaultsMap.put(key, defCopy); 946 } 947 Setting<?> prop = settingsMap.get(key); 948 if (klass.isInstance(prop)) { 949 return (T) prop; 950 } else { 951 return def; 952 } 953 } 954 955 /** 956 * Put a collection. 957 * @param key key 958 * @param value value 959 * @return {@code true}, if something has changed (i.e. value is different than before) 960 */ 961 public boolean putCollection(String key, Collection<String> value) { 962 return putSetting(key, value == null ? null : ListSetting.create(value)); 963 } 964 965 /** 966 * Saves at most {@code maxsize} items of collection {@code val}. 967 * @param key key 968 * @param maxsize max number of items to save 969 * @param val value 970 * @return {@code true}, if something has changed (i.e. value is different than before) 971 */ 972 public boolean putCollectionBounded(String key, int maxsize, Collection<String> val) { 973 Collection<String> newCollection = new ArrayList<>(Math.min(maxsize, val.size())); 974 for (String i : val) { 975 if (newCollection.size() >= maxsize) { 976 break; 977 } 978 newCollection.add(i); 979 } 980 return putCollection(key, newCollection); 981 } 982 983 /** 984 * Used to read a 2-dimensional array of strings from the preference file. 985 * If not a single entry could be found, <code>def</code> is returned. 986 * @param key preference key 987 * @param def default array value 988 * @return array value 989 */ 990 @SuppressWarnings({ "unchecked", "rawtypes" }) 991 public synchronized Collection<Collection<String>> getArray(String key, Collection<Collection<String>> def) { 992 ListListSetting val = getSetting(key, ListListSetting.create(def), ListListSetting.class); 993 return (Collection) val.getValue(); 994 } 995 996 public Collection<Collection<String>> getArray(String key) { 997 Collection<Collection<String>> res = getArray(key, null); 998 return res == null ? Collections.<Collection<String>>emptyList() : res; 999 } 1000 1001 /** 1002 * Put an array. 1003 * @param key key 1004 * @param value value 1005 * @return {@code true}, if something has changed (i.e. value is different than before) 1006 */ 1007 public boolean putArray(String key, Collection<Collection<String>> value) { 1008 return putSetting(key, value == null ? null : ListListSetting.create(value)); 1009 } 1010 1011 public Collection<Map<String, String>> getListOfStructs(String key, Collection<Map<String, String>> def) { 1012 return getSetting(key, new MapListSetting(def == null ? null : new ArrayList<>(def)), MapListSetting.class).getValue(); 1013 } 1014 1015 public boolean putListOfStructs(String key, Collection<Map<String, String>> value) { 1016 return putSetting(key, value == null ? null : new MapListSetting(new ArrayList<>(value))); 1017 } 1018 1019 /** 1020 * Annotation used for converting objects to String Maps and vice versa. 1021 * Indicates that a certain field should be considered in the conversion process. Otherwise it is ignored. 1022 * 1023 * @see #serializeStruct(java.lang.Object, java.lang.Class) 1024 * @see #deserializeStruct(java.util.Map, java.lang.Class) 1025 */ 1026 @Retention(RetentionPolicy.RUNTIME) // keep annotation at runtime 1027 public @interface pref { } 1028 1029 /** 1030 * Annotation used for converting objects to String Maps. 1031 * Indicates that a certain field should be written to the map, even if the value is the same as the default value. 1032 * 1033 * @see #serializeStruct(java.lang.Object, java.lang.Class) 1034 */ 1035 @Retention(RetentionPolicy.RUNTIME) // keep annotation at runtime 1036 public @interface writeExplicitly { } 1037 1038 /** 1039 * Get a list of hashes which are represented by a struct-like class. 1040 * Possible properties are given by fields of the class klass that have the @pref annotation. 1041 * Default constructor is used to initialize the struct objects, properties then override some of these default values. 1042 * @param <T> klass type 1043 * @param key main preference key 1044 * @param klass The struct class 1045 * @return a list of objects of type T or an empty list if nothing was found 1046 */ 1047 public <T> List<T> getListOfStructs(String key, Class<T> klass) { 1048 List<T> r = getListOfStructs(key, null, klass); 1049 if (r == null) 1050 return Collections.emptyList(); 1051 else 1052 return r; 1053 } 1054 1055 /** 1056 * same as above, but returns def if nothing was found 1057 * @param <T> klass type 1058 * @param key main preference key 1059 * @param def default value 1060 * @param klass The struct class 1061 * @return a list of objects of type T or {@code def} if nothing was found 1062 */ 1063 public <T> List<T> getListOfStructs(String key, Collection<T> def, Class<T> klass) { 1064 Collection<Map<String, String>> prop = 1065 getListOfStructs(key, def == null ? null : serializeListOfStructs(def, klass)); 1066 if (prop == null) 1067 return def == null ? null : new ArrayList<>(def); 1068 List<T> lst = new ArrayList<>(); 1069 for (Map<String, String> entries : prop) { 1070 T struct = deserializeStruct(entries, klass); 1071 lst.add(struct); 1072 } 1073 return lst; 1074 } 1075 1076 /** 1077 * Convenience method that saves a MapListSetting which is provided as a collection of objects. 1078 * 1079 * Each object is converted to a <code>Map<String, String></code> using the fields with {@link pref} annotation. 1080 * The field name is the key and the value will be converted to a string. 1081 * 1082 * Considers only fields that have the @pref annotation. 1083 * In addition it does not write fields with null values. (Thus they are cleared) 1084 * Default values are given by the field values after default constructor has been called. 1085 * Fields equal to the default value are not written unless the field has the @writeExplicitly annotation. 1086 * @param <T> the class, 1087 * @param key main preference key 1088 * @param val the list that is supposed to be saved 1089 * @param klass The struct class 1090 * @return true if something has changed 1091 */ 1092 public <T> boolean putListOfStructs(String key, Collection<T> val, Class<T> klass) { 1093 return putListOfStructs(key, serializeListOfStructs(val, klass)); 1094 } 1095 1096 private static <T> Collection<Map<String, String>> serializeListOfStructs(Collection<T> l, Class<T> klass) { 1097 if (l == null) 1098 return null; 1099 Collection<Map<String, String>> vals = new ArrayList<>(); 1100 for (T struct : l) { 1101 if (struct == null) { 1102 continue; 1103 } 1104 vals.add(serializeStruct(struct, klass)); 1105 } 1106 return vals; 1107 } 1108 1109 @SuppressWarnings("rawtypes") 1110 private static String mapToJson(Map map) { 1111 StringWriter stringWriter = new StringWriter(); 1112 try (JsonWriter writer = Json.createWriter(stringWriter)) { 1113 JsonObjectBuilder object = Json.createObjectBuilder(); 1114 for (Object o: map.entrySet()) { 1115 Entry e = (Entry) o; 1116 Object evalue = e.getValue(); 1117 object.add(e.getKey().toString(), evalue.toString()); 1118 } 1119 writer.writeObject(object.build()); 1120 } 1121 return stringWriter.toString(); 1122 } 1123 1124 @SuppressWarnings({ "rawtypes", "unchecked" }) 1125 private static Map mapFromJson(String s) { 1126 Map ret = null; 1127 try (JsonReader reader = Json.createReader(new StringReader(s))) { 1128 JsonObject object = reader.readObject(); 1129 ret = new HashMap(object.size()); 1130 for (Entry<String, JsonValue> e: object.entrySet()) { 1131 JsonValue value = e.getValue(); 1132 if (value instanceof JsonString) { 1133 // in some cases, when JsonValue.toString() is called, then additional quotation marks are left in value 1134 ret.put(e.getKey(), ((JsonString) value).getString()); 1135 } else { 1136 ret.put(e.getKey(), e.getValue().toString()); 1137 } 1138 } 1139 } 1140 return ret; 1141 } 1142 1143 @SuppressWarnings("rawtypes") 1144 private static String multiMapToJson(MultiMap map) { 1145 StringWriter stringWriter = new StringWriter(); 1146 try (JsonWriter writer = Json.createWriter(stringWriter)) { 1147 JsonObjectBuilder object = Json.createObjectBuilder(); 1148 for (Object o: map.entrySet()) { 1149 Entry e = (Entry) o; 1150 Set evalue = (Set) e.getValue(); 1151 JsonArrayBuilder a = Json.createArrayBuilder(); 1152 for (Object evo: evalue) { 1153 a.add(evo.toString()); 1154 } 1155 object.add(e.getKey().toString(), a.build()); 1156 } 1157 writer.writeObject(object.build()); 1158 } 1159 return stringWriter.toString(); 1160 } 1161 1162 @SuppressWarnings({ "rawtypes", "unchecked" }) 1163 private static MultiMap multiMapFromJson(String s) { 1164 MultiMap ret = null; 1165 try (JsonReader reader = Json.createReader(new StringReader(s))) { 1166 JsonObject object = reader.readObject(); 1167 ret = new MultiMap(object.size()); 1168 for (Entry<String, JsonValue> e: object.entrySet()) { 1169 JsonValue value = e.getValue(); 1170 if (value instanceof JsonArray) { 1171 for (JsonString js: ((JsonArray) value).getValuesAs(JsonString.class)) { 1172 ret.put(e.getKey(), js.getString()); 1173 } 1174 } else if (value instanceof JsonString) { 1175 // in some cases, when JsonValue.toString() is called, then additional quotation marks are left in value 1176 ret.put(e.getKey(), ((JsonString) value).getString()); 1177 } else { 1178 ret.put(e.getKey(), e.getValue().toString()); 1179 } 1180 } 1181 } 1182 return ret; 1183 } 1184 1185 /** 1186 * Convert an object to a String Map, by using field names and values as map key and value. 1187 * 1188 * The field value is converted to a String. 1189 * 1190 * Only fields with annotation {@link pref} are taken into account. 1191 * 1192 * Fields will not be written to the map if the value is null or unchanged 1193 * (compared to an object created with the no-arg-constructor). 1194 * The {@link writeExplicitly} annotation overrides this behavior, i.e. the default value will also be written. 1195 * 1196 * @param <T> the class of the object <code>struct</code> 1197 * @param struct the object to be converted 1198 * @param klass the class T 1199 * @return the resulting map (same data content as <code>struct</code>) 1200 */ 1201 public static <T> Map<String, String> serializeStruct(T struct, Class<T> klass) { 1202 T structPrototype; 1203 try { 1204 structPrototype = klass.getConstructor().newInstance(); 1205 } catch (ReflectiveOperationException ex) { 1206 throw new IllegalArgumentException(ex); 1207 } 1208 1209 Map<String, String> hash = new LinkedHashMap<>(); 1210 for (Field f : klass.getDeclaredFields()) { 1211 if (f.getAnnotation(pref.class) == null) { 1212 continue; 1213 } 1214 Utils.setObjectsAccessible(f); 1215 try { 1216 Object fieldValue = f.get(struct); 1217 Object defaultFieldValue = f.get(structPrototype); 1218 if (fieldValue != null && (f.getAnnotation(writeExplicitly.class) != null || !Objects.equals(fieldValue, defaultFieldValue))) { 1219 String key = f.getName().replace('_', '-'); 1220 if (fieldValue instanceof Map) { 1221 hash.put(key, mapToJson((Map<?, ?>) fieldValue)); 1222 } else if (fieldValue instanceof MultiMap) { 1223 hash.put(key, multiMapToJson((MultiMap<?, ?>) fieldValue)); 1224 } else { 1225 hash.put(key, fieldValue.toString()); 1226 } 1227 } 1228 } catch (IllegalAccessException ex) { 1229 throw new RuntimeException(ex); 1230 } 1231 } 1232 return hash; 1233 } 1234 1235 /** 1236 * Converts a String-Map to an object of a certain class, by comparing map keys to field names of the class and assigning 1237 * map values to the corresponding fields. 1238 * 1239 * The map value (a String) is converted to the field type. Supported types are: boolean, Boolean, int, Integer, double, 1240 * Double, String, Map<String, String> and Map<String, List<String>>. 1241 * 1242 * Only fields with annotation {@link pref} are taken into account. 1243 * @param <T> the class 1244 * @param hash the string map with initial values 1245 * @param klass the class T 1246 * @return an object of class T, initialized as described above 1247 */ 1248 public static <T> T deserializeStruct(Map<String, String> hash, Class<T> klass) { 1249 T struct = null; 1250 try { 1251 struct = klass.getConstructor().newInstance(); 1252 } catch (ReflectiveOperationException ex) { 1253 throw new IllegalArgumentException(ex); 1254 } 1255 for (Entry<String, String> key_value : hash.entrySet()) { 1256 Object value; 1257 Field f; 1258 try { 1259 f = klass.getDeclaredField(key_value.getKey().replace('-', '_')); 1260 } catch (NoSuchFieldException ex) { 1261 Main.trace(ex); 1262 continue; 1263 } 1264 if (f.getAnnotation(pref.class) == null) { 1265 continue; 1266 } 1267 Utils.setObjectsAccessible(f); 1268 if (f.getType() == Boolean.class || f.getType() == boolean.class) { 1269 value = Boolean.valueOf(key_value.getValue()); 1270 } else if (f.getType() == Integer.class || f.getType() == int.class) { 1271 try { 1272 value = Integer.valueOf(key_value.getValue()); 1273 } catch (NumberFormatException nfe) { 1274 continue; 1275 } 1276 } else if (f.getType() == Double.class || f.getType() == double.class) { 1277 try { 1278 value = Double.valueOf(key_value.getValue()); 1279 } catch (NumberFormatException nfe) { 1280 continue; 1281 } 1282 } else if (f.getType() == String.class) { 1283 value = key_value.getValue(); 1284 } else if (f.getType().isAssignableFrom(Map.class)) { 1285 value = mapFromJson(key_value.getValue()); 1286 } else if (f.getType().isAssignableFrom(MultiMap.class)) { 1287 value = multiMapFromJson(key_value.getValue()); 1288 } else 1289 throw new RuntimeException("unsupported preference primitive type"); 1290 1291 try { 1292 f.set(struct, value); 1293 } catch (IllegalArgumentException ex) { 1294 throw new AssertionError(ex); 1295 } catch (IllegalAccessException ex) { 1296 throw new RuntimeException(ex); 1297 } 1298 } 1299 return struct; 1300 } 1301 1302 public Map<String, Setting<?>> getAllSettings() { 1303 return new TreeMap<>(settingsMap); 1304 } 1305 1306 public Map<String, Setting<?>> getAllDefaults() { 1307 return new TreeMap<>(defaultsMap); 1308 } 1309 1310 /** 1311 * Updates system properties with the current values in the preferences. 1312 * 1313 */ 1314 public void updateSystemProperties() { 1315 if ("true".equals(get("prefer.ipv6", "auto")) && !"true".equals(Utils.updateSystemProperty("java.net.preferIPv6Addresses", "true"))) { 1316 // never set this to false, only true! 1317 Main.info(tr("Try enabling IPv6 network, prefering IPv6 over IPv4 (only works on early startup).")); 1318 } 1319 Utils.updateSystemProperty("http.agent", Version.getInstance().getAgentString()); 1320 Utils.updateSystemProperty("user.language", get("language")); 1321 // Workaround to fix a Java bug. This ugly hack comes from Sun bug database: https://bugs.openjdk.java.net/browse/JDK-6292739 1322 // Force AWT toolkit to update its internal preferences (fix #6345). 1323 if (!GraphicsEnvironment.isHeadless()) { 1324 try { 1325 Field field = Toolkit.class.getDeclaredField("resources"); 1326 Utils.setObjectsAccessible(field); 1327 field.set(null, ResourceBundle.getBundle("sun.awt.resources.awt")); 1328 } catch (ReflectiveOperationException | MissingResourceException e) { 1329 Main.warn(e); 1330 } 1331 } 1332 // Possibility to disable SNI (not by default) in case of misconfigured https servers 1333 // See #9875 + http://stackoverflow.com/a/14884941/2257172 1334 // then https://josm.openstreetmap.de/ticket/12152#comment:5 for details 1335 if (getBoolean("jdk.tls.disableSNIExtension", false)) { 1336 Utils.updateSystemProperty("jsse.enableSNIExtension", "false"); 1337 } 1338 // Workaround to fix another Java bug - The bug seems to have been fixed in Java 8, to remove during transition 1339 // Force Java 7 to use old sorting algorithm of Arrays.sort (fix #8712). 1340 // See Oracle bug database: https://bugs.openjdk.java.net/browse/JDK-7075600 1341 // and https://bugs.openjdk.java.net/browse/JDK-6923200 1342 if (getBoolean("jdk.Arrays.useLegacyMergeSort", !Version.getInstance().isLocalBuild())) { 1343 Utils.updateSystemProperty("java.util.Arrays.useLegacyMergeSort", "true"); 1344 } 1345 } 1346 1347 /** 1348 * Replies the collection of plugin site URLs from where plugin lists can be downloaded. 1349 * @return the collection of plugin site URLs 1350 * @see #getOnlinePluginSites 1351 */ 1352 public Collection<String> getPluginSites() { 1353 return getCollection("pluginmanager.sites", Collections.singleton(Main.getJOSMWebsite()+"/pluginicons%<?plugins=>")); 1354 } 1355 1356 /** 1357 * Returns the list of plugin sites available according to offline mode settings. 1358 * @return the list of available plugin sites 1359 * @since 8471 1360 */ 1361 public Collection<String> getOnlinePluginSites() { 1362 Collection<String> pluginSites = new ArrayList<>(getPluginSites()); 1363 for (Iterator<String> it = pluginSites.iterator(); it.hasNext();) { 1364 try { 1365 OnlineResource.JOSM_WEBSITE.checkOfflineAccess(it.next(), Main.getJOSMWebsite()); 1366 } catch (OfflineAccessException ex) { 1367 Main.warn(ex, false); 1368 it.remove(); 1369 } 1370 } 1371 return pluginSites; 1372 } 1373 1374 /** 1375 * Sets the collection of plugin site URLs. 1376 * 1377 * @param sites the site URLs 1378 */ 1379 public void setPluginSites(Collection<String> sites) { 1380 putCollection("pluginmanager.sites", sites); 1381 } 1382 1383 /** 1384 * Returns XML describing these preferences. 1385 * @param nopass if password must be excluded 1386 * @return XML 1387 */ 1388 public String toXML(boolean nopass) { 1389 return toXML(settingsMap.entrySet(), nopass, false); 1390 } 1391 1392 /** 1393 * Returns XML describing the given preferences. 1394 * @param settings preferences settings 1395 * @param nopass if password must be excluded 1396 * @param defaults true, if default values are converted to XML, false for 1397 * regular preferences 1398 * @return XML 1399 */ 1400 public String toXML(Collection<Entry<String, Setting<?>>> settings, boolean nopass, boolean defaults) { 1401 try ( 1402 StringWriter sw = new StringWriter(); 1403 PreferencesWriter prefWriter = new PreferencesWriter(new PrintWriter(sw), nopass, defaults); 1404 ) { 1405 prefWriter.write(settings); 1406 sw.flush(); 1407 return sw.toString(); 1408 } catch (IOException e) { 1409 Main.error(e); 1410 return null; 1411 } 1412 } 1413 1414 /** 1415 * Removes obsolete preference settings. If you throw out a once-used preference 1416 * setting, add it to the list here with an expiry date (written as comment). If you 1417 * see something with an expiry date in the past, remove it from the list. 1418 * @param loadedVersion JOSM version when the preferences file was written 1419 */ 1420 private void removeObsolete(int loadedVersion) { 1421 /* drop in October 2016 */ 1422 if (loadedVersion < 9715) { 1423 Setting<?> setting = settingsMap.get("imagery.entries"); 1424 if (setting instanceof MapListSetting) { 1425 List<Map<String, String>> l = new LinkedList<>(); 1426 boolean modified = false; 1427 for (Map<String, String> map: ((MapListSetting) setting).getValue()) { 1428 Map<String, String> newMap = new HashMap<>(); 1429 for (Entry<String, String> entry: map.entrySet()) { 1430 String value = entry.getValue(); 1431 if ("noTileHeaders".equals(entry.getKey())) { 1432 value = value.replaceFirst("\":(\".*\")\\}", "\":[$1]}"); 1433 if (!value.equals(entry.getValue())) { 1434 modified = true; 1435 } 1436 } 1437 newMap.put(entry.getKey(), value); 1438 } 1439 l.add(newMap); 1440 } 1441 if (modified) { 1442 putListOfStructs("imagery.entries", l); 1443 } 1444 } 1445 } 1446 // drop in November 2016 1447 removeUrlFromEntries(loadedVersion, 9965, 1448 "mappaint.style.entries", 1449 "josm.openstreetmap.de/josmfile?page=Styles/LegacyStandard"); 1450 // drop in December 2016 1451 removeUrlFromEntries(loadedVersion, 10063, 1452 "validator.org.openstreetmap.josm.data.validation.tests.MapCSSTagChecker.entries", 1453 "resource://data/validator/power.mapcss"); 1454 1455 for (String key : OBSOLETE_PREF_KEYS) { 1456 if (settingsMap.containsKey(key)) { 1457 settingsMap.remove(key); 1458 Main.info(tr("Preference setting {0} has been removed since it is no longer used.", key)); 1459 } 1460 } 1461 } 1462 1463 private void removeUrlFromEntries(int loadedVersion, int versionMax, String key, String urlPart) { 1464 if (loadedVersion < versionMax) { 1465 Setting<?> setting = settingsMap.get(key); 1466 if (setting instanceof MapListSetting) { 1467 List<Map<String, String>> l = new LinkedList<>(); 1468 boolean modified = false; 1469 for (Map<String, String> map: ((MapListSetting) setting).getValue()) { 1470 String url = map.get("url"); 1471 if (url != null && url.contains(urlPart)) { 1472 modified = true; 1473 } else { 1474 l.add(map); 1475 } 1476 } 1477 if (modified) { 1478 putListOfStructs(key, l); 1479 } 1480 } 1481 } 1482 } 1483 1484 /** 1485 * Enables or not the preferences file auto-save mechanism (save each time a setting is changed). 1486 * This behaviour is enabled by default. 1487 * @param enable if {@code true}, makes JOSM save preferences file each time a setting is changed 1488 * @since 7085 1489 */ 1490 public final void enableSaveOnPut(boolean enable) { 1491 synchronized (this) { 1492 saveOnPut = enable; 1493 } 1494 } 1495}