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