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