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