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