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