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.io.File; 008import java.io.IOException; 009import java.io.PrintWriter; 010import java.io.Reader; 011import java.io.StringWriter; 012import java.nio.charset.StandardCharsets; 013import java.util.ArrayList; 014import java.util.Collection; 015import java.util.Collections; 016import java.util.HashMap; 017import java.util.HashSet; 018import java.util.Iterator; 019import java.util.LinkedList; 020import java.util.List; 021import java.util.Map; 022import java.util.Map.Entry; 023import java.util.Optional; 024import java.util.Set; 025import java.util.SortedMap; 026import java.util.TreeMap; 027import java.util.concurrent.TimeUnit; 028import java.util.function.Predicate; 029import java.util.stream.Stream; 030 031import javax.swing.JOptionPane; 032import javax.xml.stream.XMLStreamException; 033 034import org.openstreetmap.josm.Main; 035import org.openstreetmap.josm.data.preferences.ColorInfo; 036import org.openstreetmap.josm.data.preferences.NamedColorProperty; 037import org.openstreetmap.josm.data.preferences.PreferencesReader; 038import org.openstreetmap.josm.data.preferences.PreferencesWriter; 039import org.openstreetmap.josm.io.OfflineAccessException; 040import org.openstreetmap.josm.io.OnlineResource; 041import org.openstreetmap.josm.spi.preferences.AbstractPreferences; 042import org.openstreetmap.josm.spi.preferences.Config; 043import org.openstreetmap.josm.spi.preferences.IBaseDirectories; 044import org.openstreetmap.josm.spi.preferences.ListSetting; 045import org.openstreetmap.josm.spi.preferences.Setting; 046import org.openstreetmap.josm.spi.preferences.StringSetting; 047import org.openstreetmap.josm.tools.CheckParameterUtil; 048import org.openstreetmap.josm.tools.ListenerList; 049import org.openstreetmap.josm.tools.Logging; 050import org.openstreetmap.josm.tools.Utils; 051import org.xml.sax.SAXException; 052 053/** 054 * This class holds all preferences for JOSM. 055 * 056 * Other classes can register their beloved properties here. All properties will be 057 * saved upon set-access. 058 * 059 * Each property is a key=setting pair, where key is a String and setting can be one of 060 * 4 types: 061 * string, list, list of lists and list of maps. 062 * In addition, each key has a unique default value that is set when the value is first 063 * accessed using one of the get...() methods. You can use the same preference 064 * key in different parts of the code, but the default value must be the same 065 * everywhere. A default value of null means, the setting has been requested, but 066 * no default value was set. This is used in advanced preferences to present a list 067 * off all possible settings. 068 * 069 * At the moment, you cannot put the empty string for string properties. 070 * put(key, "") means, the property is removed. 071 * 072 * @author imi 073 * @since 74 074 */ 075public class Preferences extends AbstractPreferences { 076 077 private static final String[] OBSOLETE_PREF_KEYS = { 078 }; 079 080 private static final long MAX_AGE_DEFAULT_PREFERENCES = TimeUnit.DAYS.toSeconds(50); 081 082 private final IBaseDirectories dirs; 083 084 /** 085 * Determines if preferences file is saved each time a property is changed. 086 */ 087 private boolean saveOnPut = true; 088 089 /** 090 * Maps the setting name to the current value of the setting. 091 * The map must not contain null as key or value. The mapped setting objects 092 * must not have a null value. 093 */ 094 protected final SortedMap<String, Setting<?>> settingsMap = new TreeMap<>(); 095 096 /** 097 * Maps the setting name to the default value of the setting. 098 * The map must not contain null as key or value. The value of the mapped 099 * setting objects can be null. 100 */ 101 protected final SortedMap<String, Setting<?>> defaultsMap = new TreeMap<>(); 102 103 private final Predicate<Entry<String, Setting<?>>> NO_DEFAULT_SETTINGS_ENTRY = 104 e -> !e.getValue().equals(defaultsMap.get(e.getKey())); 105 106 /** 107 * Indicates whether {@link #init(boolean)} completed successfully. 108 * Used to decide whether to write backup preference file in {@link #save()} 109 */ 110 protected boolean initSuccessful; 111 112 private final ListenerList<org.openstreetmap.josm.spi.preferences.PreferenceChangedListener> listeners = ListenerList.create(); 113 114 private final HashMap<String, ListenerList<org.openstreetmap.josm.spi.preferences.PreferenceChangedListener>> keyListeners = new HashMap<>(); 115 116 /** 117 * Constructs a new {@code Preferences}. 118 */ 119 public Preferences() { 120 this.dirs = Config.getDirs(); 121 } 122 123 /** 124 * Constructs a new {@code Preferences}. 125 * 126 * @param dirs the directories to use for saving the preferences 127 */ 128 public Preferences(IBaseDirectories dirs) { 129 this.dirs = dirs; 130 } 131 132 /** 133 * Constructs a new {@code Preferences} from an existing instance. 134 * @param pref existing preferences to copy 135 * @since 12634 136 */ 137 public Preferences(Preferences pref) { 138 this(pref.dirs); 139 settingsMap.putAll(pref.settingsMap); 140 defaultsMap.putAll(pref.defaultsMap); 141 } 142 143 /** 144 * Adds a new preferences listener. 145 * @param listener The listener to add 146 * @since 12881 147 */ 148 @Override 149 public void addPreferenceChangeListener(org.openstreetmap.josm.spi.preferences.PreferenceChangedListener listener) { 150 if (listener != null) { 151 listeners.addListener(listener); 152 } 153 } 154 155 /** 156 * Removes a preferences listener. 157 * @param listener The listener to remove 158 * @since 12881 159 */ 160 @Override 161 public void removePreferenceChangeListener(org.openstreetmap.josm.spi.preferences.PreferenceChangedListener listener) { 162 listeners.removeListener(listener); 163 } 164 165 /** 166 * Adds a listener that only listens to changes in one preference 167 * @param key The preference key to listen to 168 * @param listener The listener to add. 169 * @since 12881 170 */ 171 @Override 172 public void addKeyPreferenceChangeListener(String key, org.openstreetmap.josm.spi.preferences.PreferenceChangedListener listener) { 173 listenersForKey(key).addListener(listener); 174 } 175 176 /** 177 * Adds a weak listener that only listens to changes in one preference 178 * @param key The preference key to listen to 179 * @param listener The listener to add. 180 * @since 10824 181 */ 182 public void addWeakKeyPreferenceChangeListener(String key, org.openstreetmap.josm.spi.preferences.PreferenceChangedListener listener) { 183 listenersForKey(key).addWeakListener(listener); 184 } 185 186 private ListenerList<org.openstreetmap.josm.spi.preferences.PreferenceChangedListener> listenersForKey(String key) { 187 return keyListeners.computeIfAbsent(key, k -> ListenerList.create()); 188 } 189 190 /** 191 * Removes a listener that only listens to changes in one preference 192 * @param key The preference key to listen to 193 * @param listener The listener to add. 194 * @since 12881 195 */ 196 @Override 197 public void removeKeyPreferenceChangeListener(String key, org.openstreetmap.josm.spi.preferences.PreferenceChangedListener listener) { 198 Optional.ofNullable(keyListeners.get(key)).orElseThrow( 199 () -> new IllegalArgumentException("There are no listeners registered for " + key)) 200 .removeListener(listener); 201 } 202 203 protected void firePreferenceChanged(String key, Setting<?> oldValue, Setting<?> newValue) { 204 final org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent evt = 205 new org.openstreetmap.josm.spi.preferences.DefaultPreferenceChangeEvent(key, oldValue, newValue); 206 listeners.fireEvent(listener -> listener.preferenceChanged(evt)); 207 208 ListenerList<org.openstreetmap.josm.spi.preferences.PreferenceChangedListener> forKey = keyListeners.get(key); 209 if (forKey != null) { 210 forKey.fireEvent(listener -> listener.preferenceChanged(evt)); 211 } 212 } 213 214 /** 215 * Get the base name of the JOSM directories for preferences, cache and user data. 216 * Default value is "JOSM", unless overridden by system property "josm.dir.name". 217 * @return the base name of the JOSM directories for preferences, cache and user data 218 */ 219 public String getJOSMDirectoryBaseName() { 220 String name = System.getProperty("josm.dir.name"); 221 if (name != null) 222 return name; 223 else 224 return "JOSM"; 225 } 226 227 /** 228 * Get the base directories associated with this preference instance. 229 * @return the base directories 230 */ 231 public IBaseDirectories getDirs() { 232 return dirs; 233 } 234 235 /** 236 * Returns the user defined preferences directory, containing the preferences.xml file 237 * @return The user defined preferences directory, containing the preferences.xml file 238 * @since 7834 239 * @deprecated use {@link #getPreferencesDirectory(boolean)} 240 */ 241 @Deprecated 242 public File getPreferencesDirectory() { 243 return getPreferencesDirectory(false); 244 } 245 246 /** 247 * @param createIfMissing if true, automatically creates this directory, 248 * in case it is missing 249 * @return the preferences directory 250 * @deprecated use {@link #getDirs()} or (more generally) {@link Config#getDirs()} 251 */ 252 @Deprecated 253 public File getPreferencesDirectory(boolean createIfMissing) { 254 return dirs.getPreferencesDirectory(createIfMissing); 255 } 256 257 /** 258 * Returns the user data directory, containing autosave, plugins, etc. 259 * Depending on the OS it may be the same directory as preferences directory. 260 * @return The user data directory, containing autosave, plugins, etc. 261 * @since 7834 262 * @deprecated use {@link #getUserDataDirectory(boolean)} 263 */ 264 @Deprecated 265 public File getUserDataDirectory() { 266 return getUserDataDirectory(false); 267 } 268 269 /** 270 * @param createIfMissing if true, automatically creates this directory, 271 * in case it is missing 272 * @return the user data directory 273 * @deprecated use {@link #getDirs()} or (more generally) {@link Config#getDirs()} 274 */ 275 @Deprecated 276 public File getUserDataDirectory(boolean createIfMissing) { 277 return dirs.getUserDataDirectory(createIfMissing); 278 } 279 280 /** 281 * Returns the user preferences file (preferences.xml). 282 * @return The user preferences file (preferences.xml) 283 */ 284 public File getPreferenceFile() { 285 return new File(dirs.getPreferencesDirectory(false), "preferences.xml"); 286 } 287 288 /** 289 * Returns the cache file for default preferences. 290 * @return the cache file for default preferences 291 */ 292 public File getDefaultsCacheFile() { 293 return new File(dirs.getCacheDirectory(true), "default_preferences.xml"); 294 } 295 296 /** 297 * Returns the user plugin directory. 298 * @return The user plugin directory 299 */ 300 public File getPluginsDirectory() { 301 return new File(dirs.getUserDataDirectory(false), "plugins"); 302 } 303 304 /** 305 * Get the directory where cached content of any kind should be stored. 306 * 307 * If the directory doesn't exist on the file system, it will be created by this method. 308 * 309 * @return the cache directory 310 * @deprecated use {@link #getCacheDirectory(boolean)} 311 */ 312 @Deprecated 313 public File getCacheDirectory() { 314 return getCacheDirectory(true); 315 } 316 317 /** 318 * @param createIfMissing if true, automatically creates this directory, 319 * in case it is missing 320 * @return the cache directory 321 * @deprecated use {@link #getDirs()} or (more generally) {@link Config#getDirs()} 322 */ 323 @Deprecated 324 public File getCacheDirectory(boolean createIfMissing) { 325 return dirs.getCacheDirectory(createIfMissing); 326 } 327 328 private static void addPossibleResourceDir(Set<String> locations, String s) { 329 if (s != null) { 330 if (!s.endsWith(File.separator)) { 331 s += File.separator; 332 } 333 locations.add(s); 334 } 335 } 336 337 /** 338 * Returns a set of all existing directories where resources could be stored. 339 * @return A set of all existing directories where resources could be stored. 340 */ 341 public Collection<String> getAllPossiblePreferenceDirs() { 342 Set<String> locations = new HashSet<>(); 343 addPossibleResourceDir(locations, dirs.getPreferencesDirectory(false).getPath()); 344 addPossibleResourceDir(locations, dirs.getUserDataDirectory(false).getPath()); 345 addPossibleResourceDir(locations, System.getenv("JOSM_RESOURCES")); 346 addPossibleResourceDir(locations, System.getProperty("josm.resources")); 347 if (Main.isPlatformWindows()) { 348 String appdata = System.getenv("APPDATA"); 349 if (appdata != null && System.getenv("ALLUSERSPROFILE") != null 350 && appdata.lastIndexOf(File.separator) != -1) { 351 appdata = appdata.substring(appdata.lastIndexOf(File.separator)); 352 locations.add(new File(new File(System.getenv("ALLUSERSPROFILE"), 353 appdata), "JOSM").getPath()); 354 } 355 } else { 356 locations.add("/usr/local/share/josm/"); 357 locations.add("/usr/local/lib/josm/"); 358 locations.add("/usr/share/josm/"); 359 locations.add("/usr/lib/josm/"); 360 } 361 return locations; 362 } 363 364 /** 365 * Gets all normal (string) settings that have a key starting with the prefix 366 * @param prefix The start of the key 367 * @return The key names of the settings 368 */ 369 public synchronized Map<String, String> getAllPrefix(final String prefix) { 370 final Map<String, String> all = new TreeMap<>(); 371 for (final Entry<String, Setting<?>> e : settingsMap.entrySet()) { 372 if (e.getKey().startsWith(prefix) && (e.getValue() instanceof StringSetting)) { 373 all.put(e.getKey(), ((StringSetting) e.getValue()).getValue()); 374 } 375 } 376 return all; 377 } 378 379 /** 380 * Gets all list settings that have a key starting with the prefix 381 * @param prefix The start of the key 382 * @return The key names of the list settings 383 */ 384 public synchronized List<String> getAllPrefixCollectionKeys(final String prefix) { 385 final List<String> all = new LinkedList<>(); 386 for (Map.Entry<String, Setting<?>> entry : settingsMap.entrySet()) { 387 if (entry.getKey().startsWith(prefix) && entry.getValue() instanceof ListSetting) { 388 all.add(entry.getKey()); 389 } 390 } 391 return all; 392 } 393 394 /** 395 * Get all named colors, including customized and the default ones. 396 * @return a map of all named colors (maps preference key to {@link ColorInfo}) 397 */ 398 public synchronized Map<String, ColorInfo> getAllNamedColors() { 399 final Map<String, ColorInfo> all = new TreeMap<>(); 400 for (final Entry<String, Setting<?>> e : settingsMap.entrySet()) { 401 if (!e.getKey().startsWith(NamedColorProperty.NAMED_COLOR_PREFIX)) 402 continue; 403 Utils.instanceOfAndCast(e.getValue(), ListSetting.class) 404 .map(d -> d.getValue()) 405 .map(lst -> ColorInfo.fromPref(lst, false)) 406 .ifPresent(info -> all.put(e.getKey(), info)); 407 } 408 for (final Entry<String, Setting<?>> e : defaultsMap.entrySet()) { 409 if (!e.getKey().startsWith(NamedColorProperty.NAMED_COLOR_PREFIX)) 410 continue; 411 Utils.instanceOfAndCast(e.getValue(), ListSetting.class) 412 .map(d -> d.getValue()) 413 .map(lst -> ColorInfo.fromPref(lst, true)) 414 .ifPresent(infoDef -> { 415 ColorInfo info = all.get(e.getKey()); 416 if (info == null) { 417 all.put(e.getKey(), infoDef); 418 } else { 419 info.setDefaultValue(infoDef.getDefaultValue()); 420 } 421 }); 422 } 423 return all; 424 } 425 426 /** 427 * Called after every put. In case of a problem, do nothing but output the error in log. 428 * @throws IOException if any I/O error occurs 429 */ 430 public synchronized void save() throws IOException { 431 save(getPreferenceFile(), settingsMap.entrySet().stream().filter(NO_DEFAULT_SETTINGS_ENTRY), false); 432 } 433 434 /** 435 * Stores the defaults to the defaults file 436 * @throws IOException If the file could not be saved 437 */ 438 public synchronized void saveDefaults() throws IOException { 439 save(getDefaultsCacheFile(), defaultsMap.entrySet().stream(), true); 440 } 441 442 protected void save(File prefFile, Stream<Entry<String, Setting<?>>> settings, boolean defaults) throws IOException { 443 if (!defaults) { 444 /* currently unused, but may help to fix configuration issues in future */ 445 putInt("josm.version", Version.getInstance().getVersion()); 446 } 447 448 File backupFile = new File(prefFile + "_backup"); 449 450 // Backup old preferences if there are old preferences 451 if (initSuccessful && prefFile.exists() && prefFile.length() > 0) { 452 Utils.copyFile(prefFile, backupFile); 453 } 454 455 try (PreferencesWriter writer = new PreferencesWriter( 456 new PrintWriter(new File(prefFile + "_tmp"), StandardCharsets.UTF_8.name()), false, defaults)) { 457 writer.write(settings); 458 } 459 460 File tmpFile = new File(prefFile + "_tmp"); 461 Utils.copyFile(tmpFile, prefFile); 462 Utils.deleteFile(tmpFile, marktr("Unable to delete temporary file {0}")); 463 464 setCorrectPermissions(prefFile); 465 setCorrectPermissions(backupFile); 466 } 467 468 private static void setCorrectPermissions(File file) { 469 if (!file.setReadable(false, false) && Logging.isTraceEnabled()) { 470 Logging.trace(tr("Unable to set file non-readable {0}", file.getAbsolutePath())); 471 } 472 if (!file.setWritable(false, false) && Logging.isTraceEnabled()) { 473 Logging.trace(tr("Unable to set file non-writable {0}", file.getAbsolutePath())); 474 } 475 if (!file.setExecutable(false, false) && Logging.isTraceEnabled()) { 476 Logging.trace(tr("Unable to set file non-executable {0}", file.getAbsolutePath())); 477 } 478 if (!file.setReadable(true, true) && Logging.isTraceEnabled()) { 479 Logging.trace(tr("Unable to set file readable {0}", file.getAbsolutePath())); 480 } 481 if (!file.setWritable(true, true) && Logging.isTraceEnabled()) { 482 Logging.trace(tr("Unable to set file writable {0}", file.getAbsolutePath())); 483 } 484 } 485 486 /** 487 * Loads preferences from settings file. 488 * @throws IOException if any I/O error occurs while reading the file 489 * @throws SAXException if the settings file does not contain valid XML 490 * @throws XMLStreamException if an XML error occurs while parsing the file (after validation) 491 */ 492 protected void load() throws IOException, SAXException, XMLStreamException { 493 File pref = getPreferenceFile(); 494 PreferencesReader.validateXML(pref); 495 PreferencesReader reader = new PreferencesReader(pref, false); 496 reader.parse(); 497 settingsMap.clear(); 498 settingsMap.putAll(reader.getSettings()); 499 removeObsolete(reader.getVersion()); 500 } 501 502 /** 503 * Loads default preferences from default settings cache file. 504 * 505 * Discards entries older than {@link #MAX_AGE_DEFAULT_PREFERENCES}. 506 * 507 * @throws IOException if any I/O error occurs while reading the file 508 * @throws SAXException if the settings file does not contain valid XML 509 * @throws XMLStreamException if an XML error occurs while parsing the file (after validation) 510 */ 511 protected void loadDefaults() throws IOException, XMLStreamException, SAXException { 512 File def = getDefaultsCacheFile(); 513 PreferencesReader.validateXML(def); 514 PreferencesReader reader = new PreferencesReader(def, true); 515 reader.parse(); 516 defaultsMap.clear(); 517 long minTime = System.currentTimeMillis() / 1000 - MAX_AGE_DEFAULT_PREFERENCES; 518 for (Entry<String, Setting<?>> e : reader.getSettings().entrySet()) { 519 if (e.getValue().getTime() >= minTime) { 520 defaultsMap.put(e.getKey(), e.getValue()); 521 } 522 } 523 } 524 525 /** 526 * Loads preferences from XML reader. 527 * @param in XML reader 528 * @throws XMLStreamException if any XML stream error occurs 529 * @throws IOException if any I/O error occurs 530 */ 531 public void fromXML(Reader in) throws XMLStreamException, IOException { 532 PreferencesReader reader = new PreferencesReader(in, false); 533 reader.parse(); 534 settingsMap.clear(); 535 settingsMap.putAll(reader.getSettings()); 536 } 537 538 /** 539 * Initializes preferences. 540 * @param reset if {@code true}, current settings file is replaced by the default one 541 */ 542 public void init(boolean reset) { 543 initSuccessful = false; 544 // get the preferences. 545 File prefDir = dirs.getPreferencesDirectory(false); 546 if (prefDir.exists()) { 547 if (!prefDir.isDirectory()) { 548 Logging.warn(tr("Failed to initialize preferences. Preference directory ''{0}'' is not a directory.", 549 prefDir.getAbsoluteFile())); 550 JOptionPane.showMessageDialog( 551 Main.parent, 552 tr("<html>Failed to initialize preferences.<br>Preference directory ''{0}'' is not a directory.</html>", 553 prefDir.getAbsoluteFile()), 554 tr("Error"), 555 JOptionPane.ERROR_MESSAGE 556 ); 557 return; 558 } 559 } else { 560 if (!prefDir.mkdirs()) { 561 Logging.warn(tr("Failed to initialize preferences. Failed to create missing preference directory: {0}", 562 prefDir.getAbsoluteFile())); 563 JOptionPane.showMessageDialog( 564 Main.parent, 565 tr("<html>Failed to initialize preferences.<br>Failed to create missing preference directory: {0}</html>", 566 prefDir.getAbsoluteFile()), 567 tr("Error"), 568 JOptionPane.ERROR_MESSAGE 569 ); 570 return; 571 } 572 } 573 574 File preferenceFile = getPreferenceFile(); 575 try { 576 if (!preferenceFile.exists()) { 577 Logging.info(tr("Missing preference file ''{0}''. Creating a default preference file.", preferenceFile.getAbsoluteFile())); 578 resetToDefault(); 579 save(); 580 } else if (reset) { 581 File backupFile = new File(prefDir, "preferences.xml.bak"); 582 Main.platform.rename(preferenceFile, backupFile); 583 Logging.warn(tr("Replacing existing preference file ''{0}'' with default preference file.", preferenceFile.getAbsoluteFile())); 584 resetToDefault(); 585 save(); 586 } 587 } catch (IOException e) { 588 Logging.error(e); 589 JOptionPane.showMessageDialog( 590 Main.parent, 591 tr("<html>Failed to initialize preferences.<br>Failed to reset preference file to default: {0}</html>", 592 getPreferenceFile().getAbsoluteFile()), 593 tr("Error"), 594 JOptionPane.ERROR_MESSAGE 595 ); 596 return; 597 } 598 try { 599 load(); 600 initSuccessful = true; 601 } catch (IOException | SAXException | XMLStreamException e) { 602 Logging.error(e); 603 File backupFile = new File(prefDir, "preferences.xml.bak"); 604 JOptionPane.showMessageDialog( 605 Main.parent, 606 tr("<html>Preferences file had errors.<br> Making backup of old one to <br>{0}<br> " + 607 "and creating a new default preference file.</html>", 608 backupFile.getAbsoluteFile()), 609 tr("Error"), 610 JOptionPane.ERROR_MESSAGE 611 ); 612 Main.platform.rename(preferenceFile, backupFile); 613 try { 614 resetToDefault(); 615 save(); 616 } catch (IOException e1) { 617 Logging.error(e1); 618 Logging.warn(tr("Failed to initialize preferences. Failed to reset preference file to default: {0}", getPreferenceFile())); 619 } 620 } 621 File def = getDefaultsCacheFile(); 622 if (def.exists()) { 623 try { 624 loadDefaults(); 625 } catch (IOException | XMLStreamException | SAXException e) { 626 Logging.error(e); 627 Logging.warn(tr("Failed to load defaults cache file: {0}", def)); 628 defaultsMap.clear(); 629 if (!def.delete()) { 630 Logging.warn(tr("Failed to delete faulty defaults cache file: {0}", def)); 631 } 632 } 633 } 634 } 635 636 /** 637 * Resets the preferences to their initial state. This resets all values and file associations. 638 * The default values and listeners are not removed. 639 * <p> 640 * It is meant to be called before {@link #init(boolean)} 641 * @since 10876 642 */ 643 public void resetToInitialState() { 644 resetToDefault(); 645 saveOnPut = true; 646 initSuccessful = false; 647 } 648 649 /** 650 * Reset all values stored in this map to the default values. This clears the preferences. 651 */ 652 public final void resetToDefault() { 653 settingsMap.clear(); 654 } 655 656 /** 657 * Set a value for a certain setting. The changed setting is saved to the preference file immediately. 658 * Due to caching mechanisms on modern operating systems and hardware, this shouldn't be a performance problem. 659 * @param key the unique identifier for the setting 660 * @param setting the value of the setting. In case it is null, the key-value entry will be removed. 661 * @return {@code true}, if something has changed (i.e. value is different than before) 662 */ 663 @Override 664 public boolean putSetting(final String key, Setting<?> setting) { 665 CheckParameterUtil.ensureParameterNotNull(key); 666 if (setting != null && setting.getValue() == null) 667 throw new IllegalArgumentException("setting argument must not have null value"); 668 Setting<?> settingOld; 669 Setting<?> settingCopy = null; 670 synchronized (this) { 671 if (setting == null) { 672 settingOld = settingsMap.remove(key); 673 if (settingOld == null) 674 return false; 675 } else { 676 settingOld = settingsMap.get(key); 677 if (setting.equals(settingOld)) 678 return false; 679 if (settingOld == null && setting.equals(defaultsMap.get(key))) 680 return false; 681 settingCopy = setting.copy(); 682 settingsMap.put(key, settingCopy); 683 } 684 if (saveOnPut) { 685 try { 686 save(); 687 } catch (IOException e) { 688 Logging.log(Logging.LEVEL_WARN, tr("Failed to persist preferences to ''{0}''", getPreferenceFile().getAbsoluteFile()), e); 689 } 690 } 691 } 692 // Call outside of synchronized section in case some listener wait for other thread that wait for preference lock 693 firePreferenceChanged(key, settingOld, settingCopy); 694 return true; 695 } 696 697 /** 698 * Get a setting of any type 699 * @param key The key for the setting 700 * @param def The default value to use if it was not found 701 * @return The setting 702 */ 703 public synchronized Setting<?> getSetting(String key, Setting<?> def) { 704 return getSetting(key, def, Setting.class); 705 } 706 707 /** 708 * Get settings value for a certain key and provide default a value. 709 * @param <T> the setting type 710 * @param key the identifier for the setting 711 * @param def the default value. For each call of getSetting() with a given key, the default value must be the same. 712 * <code>def</code> must not be null, but the value of <code>def</code> can be null. 713 * @param klass the setting type (same as T) 714 * @return the corresponding value if the property has been set before, {@code def} otherwise 715 */ 716 @SuppressWarnings("unchecked") 717 @Override 718 public synchronized <T extends Setting<?>> T getSetting(String key, T def, Class<T> klass) { 719 CheckParameterUtil.ensureParameterNotNull(key); 720 CheckParameterUtil.ensureParameterNotNull(def); 721 Setting<?> oldDef = defaultsMap.get(key); 722 if (oldDef != null && oldDef.isNew() && oldDef.getValue() != null && def.getValue() != null && !def.equals(oldDef)) { 723 Logging.info("Defaults for " + key + " differ: " + def + " != " + defaultsMap.get(key)); 724 } 725 if (def.getValue() != null || oldDef == null) { 726 Setting<?> defCopy = def.copy(); 727 defCopy.setTime(System.currentTimeMillis() / 1000); 728 defCopy.setNew(true); 729 defaultsMap.put(key, defCopy); 730 } 731 Setting<?> prop = settingsMap.get(key); 732 if (klass.isInstance(prop)) { 733 return (T) prop; 734 } else { 735 return def; 736 } 737 } 738 739 @Override 740 public Set<String> getKeySet() { 741 return Collections.unmodifiableSet(settingsMap.keySet()); 742 } 743 744 /** 745 * Gets a map of all settings that are currently stored 746 * @return The settings 747 */ 748 public Map<String, Setting<?>> getAllSettings() { 749 return new TreeMap<>(settingsMap); 750 } 751 752 /** 753 * Gets a map of all currently known defaults 754 * @return The map (key/setting) 755 */ 756 public Map<String, Setting<?>> getAllDefaults() { 757 return new TreeMap<>(defaultsMap); 758 } 759 760 /** 761 * Replies the collection of plugin site URLs from where plugin lists can be downloaded. 762 * @return the collection of plugin site URLs 763 * @see #getOnlinePluginSites 764 */ 765 public Collection<String> getPluginSites() { 766 return getList("pluginmanager.sites", Collections.singletonList(Main.getJOSMWebsite()+"/pluginicons%<?plugins=>")); 767 } 768 769 /** 770 * Returns the list of plugin sites available according to offline mode settings. 771 * @return the list of available plugin sites 772 * @since 8471 773 */ 774 public Collection<String> getOnlinePluginSites() { 775 Collection<String> pluginSites = new ArrayList<>(getPluginSites()); 776 for (Iterator<String> it = pluginSites.iterator(); it.hasNext();) { 777 try { 778 OnlineResource.JOSM_WEBSITE.checkOfflineAccess(it.next(), Main.getJOSMWebsite()); 779 } catch (OfflineAccessException ex) { 780 Logging.log(Logging.LEVEL_WARN, ex); 781 it.remove(); 782 } 783 } 784 return pluginSites; 785 } 786 787 /** 788 * Sets the collection of plugin site URLs. 789 * 790 * @param sites the site URLs 791 */ 792 public void setPluginSites(Collection<String> sites) { 793 putList("pluginmanager.sites", new ArrayList<>(sites)); 794 } 795 796 /** 797 * Returns XML describing these preferences. 798 * @param nopass if password must be excluded 799 * @return XML 800 */ 801 public String toXML(boolean nopass) { 802 return toXML(settingsMap.entrySet(), nopass, false); 803 } 804 805 /** 806 * Returns XML describing the given preferences. 807 * @param settings preferences settings 808 * @param nopass if password must be excluded 809 * @param defaults true, if default values are converted to XML, false for 810 * regular preferences 811 * @return XML 812 */ 813 public String toXML(Collection<Entry<String, Setting<?>>> settings, boolean nopass, boolean defaults) { 814 try ( 815 StringWriter sw = new StringWriter(); 816 PreferencesWriter prefWriter = new PreferencesWriter(new PrintWriter(sw), nopass, defaults) 817 ) { 818 prefWriter.write(settings); 819 sw.flush(); 820 return sw.toString(); 821 } catch (IOException e) { 822 Logging.error(e); 823 return null; 824 } 825 } 826 827 /** 828 * Removes obsolete preference settings. If you throw out a once-used preference 829 * setting, add it to the list here with an expiry date (written as comment). If you 830 * see something with an expiry date in the past, remove it from the list. 831 * @param loadedVersion JOSM version when the preferences file was written 832 */ 833 private void removeObsolete(int loadedVersion) { 834 for (String key : OBSOLETE_PREF_KEYS) { 835 if (settingsMap.containsKey(key)) { 836 settingsMap.remove(key); 837 Logging.info(tr("Preference setting {0} has been removed since it is no longer used.", key)); 838 } 839 } 840 } 841 842 /** 843 * Enables or not the preferences file auto-save mechanism (save each time a setting is changed). 844 * This behaviour is enabled by default. 845 * @param enable if {@code true}, makes JOSM save preferences file each time a setting is changed 846 * @since 7085 847 */ 848 public final void enableSaveOnPut(boolean enable) { 849 synchronized (this) { 850 saveOnPut = enable; 851 } 852 } 853}