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