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