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; 006 007import java.awt.Color; 008import java.awt.GraphicsEnvironment; 009import java.awt.Toolkit; 010import java.io.File; 011import java.io.IOException; 012import java.io.PrintWriter; 013import java.io.Reader; 014import java.io.StringReader; 015import java.io.StringWriter; 016import java.lang.annotation.Retention; 017import java.lang.annotation.RetentionPolicy; 018import java.lang.reflect.Field; 019import java.nio.charset.StandardCharsets; 020import java.util.AbstractMap; 021import java.util.ArrayList; 022import java.util.Collection; 023import java.util.Collections; 024import java.util.HashMap; 025import java.util.HashSet; 026import java.util.Iterator; 027import java.util.LinkedHashMap; 028import java.util.LinkedList; 029import java.util.List; 030import java.util.Map; 031import java.util.Map.Entry; 032import java.util.MissingResourceException; 033import java.util.Objects; 034import java.util.ResourceBundle; 035import java.util.Set; 036import java.util.SortedMap; 037import java.util.TreeMap; 038import java.util.function.Predicate; 039import java.util.regex.Matcher; 040import java.util.regex.Pattern; 041import java.util.stream.Collectors; 042import java.util.stream.Stream; 043 044import javax.json.Json; 045import javax.json.JsonArray; 046import javax.json.JsonArrayBuilder; 047import javax.json.JsonObject; 048import javax.json.JsonObjectBuilder; 049import javax.json.JsonReader; 050import javax.json.JsonString; 051import javax.json.JsonValue; 052import javax.json.JsonWriter; 053import javax.swing.JOptionPane; 054import javax.xml.stream.XMLStreamException; 055 056import org.openstreetmap.josm.Main; 057import org.openstreetmap.josm.data.preferences.BooleanProperty; 058import org.openstreetmap.josm.data.preferences.ColorProperty; 059import org.openstreetmap.josm.data.preferences.DoubleProperty; 060import org.openstreetmap.josm.data.preferences.IntegerProperty; 061import org.openstreetmap.josm.data.preferences.ListListSetting; 062import org.openstreetmap.josm.data.preferences.ListSetting; 063import org.openstreetmap.josm.data.preferences.LongProperty; 064import org.openstreetmap.josm.data.preferences.MapListSetting; 065import org.openstreetmap.josm.data.preferences.PreferencesReader; 066import org.openstreetmap.josm.data.preferences.PreferencesWriter; 067import org.openstreetmap.josm.data.preferences.Setting; 068import org.openstreetmap.josm.data.preferences.StringSetting; 069import org.openstreetmap.josm.io.OfflineAccessException; 070import org.openstreetmap.josm.io.OnlineResource; 071import org.openstreetmap.josm.tools.CheckParameterUtil; 072import org.openstreetmap.josm.tools.ColorHelper; 073import org.openstreetmap.josm.tools.I18n; 074import org.openstreetmap.josm.tools.ListenerList; 075import org.openstreetmap.josm.tools.MultiMap; 076import org.openstreetmap.josm.tools.Utils; 077import org.xml.sax.SAXException; 078 079/** 080 * This class holds all preferences for JOSM. 081 * 082 * Other classes can register their beloved properties here. All properties will be 083 * saved upon set-access. 084 * 085 * Each property is a key=setting pair, where key is a String and setting can be one of 086 * 4 types: 087 * string, list, list of lists and list of maps. 088 * In addition, each key has a unique default value that is set when the value is first 089 * accessed using one of the get...() methods. You can use the same preference 090 * key in different parts of the code, but the default value must be the same 091 * everywhere. A default value of null means, the setting has been requested, but 092 * no default value was set. This is used in advanced preferences to present a list 093 * off all possible settings. 094 * 095 * At the moment, you cannot put the empty string for string properties. 096 * put(key, "") means, the property is removed. 097 * 098 * @author imi 099 * @since 74 100 */ 101public class Preferences { 102 103 private static final String[] OBSOLETE_PREF_KEYS = { 104 "hdop.factor" /* remove entry after April 2017 */ 105 }; 106 107 private static final long MAX_AGE_DEFAULT_PREFERENCES = 60L * 60L * 24L * 50L; // 50 days (in seconds) 108 109 /** 110 * Internal storage for the preference directory. 111 * Do not access this variable directly! 112 * @see #getPreferencesDirectory() 113 */ 114 private File preferencesDir; 115 116 /** 117 * Internal storage for the cache directory. 118 */ 119 private File cacheDir; 120 121 /** 122 * Internal storage for the user data directory. 123 */ 124 private File userdataDir; 125 126 /** 127 * Determines if preferences file is saved each time a property is changed. 128 */ 129 private boolean saveOnPut = true; 130 131 /** 132 * Maps the setting name to the current value of the setting. 133 * The map must not contain null as key or value. The mapped setting objects 134 * must not have a null value. 135 */ 136 protected final SortedMap<String, Setting<?>> settingsMap = new TreeMap<>(); 137 138 /** 139 * Maps the setting name to the default value of the setting. 140 * The map must not contain null as key or value. The value of the mapped 141 * setting objects can be null. 142 */ 143 protected final SortedMap<String, Setting<?>> defaultsMap = new TreeMap<>(); 144 145 private final Predicate<Entry<String, Setting<?>>> NO_DEFAULT_SETTINGS_ENTRY = 146 e -> !e.getValue().equals(defaultsMap.get(e.getKey())); 147 148 /** 149 * Maps color keys to human readable color name 150 */ 151 protected final SortedMap<String, String> colornames = new TreeMap<>(); 152 153 /** 154 * Indicates whether {@link #init(boolean)} completed successfully. 155 * Used to decide whether to write backup preference file in {@link #save()} 156 */ 157 protected boolean initSuccessful; 158 159 /** 160 * Event triggered when a preference entry value changes. 161 */ 162 public interface PreferenceChangeEvent { 163 /** 164 * Returns the preference key. 165 * @return the preference key 166 */ 167 String getKey(); 168 169 /** 170 * Returns the old preference value. 171 * @return the old preference value 172 */ 173 Setting<?> getOldValue(); 174 175 /** 176 * Returns the new preference value. 177 * @return the new preference value 178 */ 179 Setting<?> getNewValue(); 180 } 181 182 /** 183 * Listener to preference change events. 184 * @since 10600 (functional interface) 185 */ 186 @FunctionalInterface 187 public interface PreferenceChangedListener { 188 /** 189 * Trigerred when a preference entry value changes. 190 * @param e the preference change event 191 */ 192 void preferenceChanged(PreferenceChangeEvent e); 193 } 194 195 private static class DefaultPreferenceChangeEvent implements PreferenceChangeEvent { 196 private final String key; 197 private final Setting<?> oldValue; 198 private final Setting<?> newValue; 199 200 DefaultPreferenceChangeEvent(String key, Setting<?> oldValue, Setting<?> newValue) { 201 this.key = key; 202 this.oldValue = oldValue; 203 this.newValue = newValue; 204 } 205 206 @Override 207 public String getKey() { 208 return key; 209 } 210 211 @Override 212 public Setting<?> getOldValue() { 213 return oldValue; 214 } 215 216 @Override 217 public Setting<?> getNewValue() { 218 return newValue; 219 } 220 } 221 222 private final ListenerList<PreferenceChangedListener> listeners = ListenerList.create(); 223 224 private final HashMap<String, ListenerList<PreferenceChangedListener>> keyListeners = new HashMap<>(); 225 226 /** 227 * Adds a new preferences listener. 228 * @param listener The listener to add 229 */ 230 public void addPreferenceChangeListener(PreferenceChangedListener listener) { 231 if (listener != null) { 232 listeners.addListener(listener); 233 } 234 } 235 236 /** 237 * Removes a preferences listener. 238 * @param listener The listener to remove 239 */ 240 public void removePreferenceChangeListener(PreferenceChangedListener listener) { 241 listeners.removeListener(listener); 242 } 243 244 /** 245 * Adds a listener that only listens to changes in one preference 246 * @param key The preference key to listen to 247 * @param listener The listener to add. 248 * @since 10824 249 */ 250 public void addKeyPreferenceChangeListener(String key, PreferenceChangedListener listener) { 251 listenersForKey(key).addListener(listener); 252 } 253 254 /** 255 * Adds a weak listener that only listens to changes in one preference 256 * @param key The preference key to listen to 257 * @param listener The listener to add. 258 * @since 10824 259 */ 260 public void addWeakKeyPreferenceChangeListener(String key, PreferenceChangedListener listener) { 261 listenersForKey(key).addWeakListener(listener); 262 } 263 264 private ListenerList<PreferenceChangedListener> listenersForKey(String key) { 265 ListenerList<PreferenceChangedListener> keyListener = keyListeners.get(key); 266 if (keyListener == null) { 267 keyListener = ListenerList.create(); 268 keyListeners.put(key, keyListener); 269 } 270 return keyListener; 271 } 272 273 /** 274 * Removes a listener that only listens to changes in one preference 275 * @param key The preference key to listen to 276 * @param listener The listener to add. 277 */ 278 public void removeKeyPreferenceChangeListener(String key, PreferenceChangedListener listener) { 279 ListenerList<PreferenceChangedListener> keyListener = keyListeners.get(key); 280 if (keyListener == null) { 281 throw new IllegalArgumentException("There are no listeners registered for " + key); 282 } 283 keyListener.removeListener(listener); 284 } 285 286 protected void firePreferenceChanged(String key, Setting<?> oldValue, Setting<?> newValue) { 287 final PreferenceChangeEvent evt = new DefaultPreferenceChangeEvent(key, oldValue, newValue); 288 listeners.fireEvent(listener -> listener.preferenceChanged(evt)); 289 290 ListenerList<PreferenceChangedListener> forKey = keyListeners.get(key); 291 if (forKey != null) { 292 forKey.fireEvent(listener -> listener.preferenceChanged(evt)); 293 } 294 } 295 296 /** 297 * Get the base name of the JOSM directories for preferences, cache and 298 * user data. 299 * Default value is "JOSM", unless overridden by system property "josm.dir.name". 300 * @return the base name of the JOSM directories for preferences, cache and 301 * user data 302 */ 303 public String getJOSMDirectoryBaseName() { 304 String name = System.getProperty("josm.dir.name"); 305 if (name != null) 306 return name; 307 else 308 return "JOSM"; 309 } 310 311 /** 312 * Returns the user defined preferences directory, containing the preferences.xml file 313 * @return The user defined preferences directory, containing the preferences.xml file 314 * @since 7834 315 */ 316 public File getPreferencesDirectory() { 317 if (preferencesDir != null) 318 return preferencesDir; 319 String path; 320 path = System.getProperty("josm.pref"); 321 if (path != null) { 322 preferencesDir = new File(path).getAbsoluteFile(); 323 } else { 324 path = System.getProperty("josm.home"); 325 if (path != null) { 326 preferencesDir = new File(path).getAbsoluteFile(); 327 } else { 328 preferencesDir = Main.platform.getDefaultPrefDirectory(); 329 } 330 } 331 return preferencesDir; 332 } 333 334 /** 335 * Returns the user data directory, containing autosave, plugins, etc. 336 * Depending on the OS it may be the same directory as preferences directory. 337 * @return The user data directory, containing autosave, plugins, etc. 338 * @since 7834 339 */ 340 public File getUserDataDirectory() { 341 if (userdataDir != null) 342 return userdataDir; 343 String path; 344 path = System.getProperty("josm.userdata"); 345 if (path != null) { 346 userdataDir = new File(path).getAbsoluteFile(); 347 } else { 348 path = System.getProperty("josm.home"); 349 if (path != null) { 350 userdataDir = new File(path).getAbsoluteFile(); 351 } else { 352 userdataDir = Main.platform.getDefaultUserDataDirectory(); 353 } 354 } 355 return userdataDir; 356 } 357 358 /** 359 * Returns the user preferences file (preferences.xml). 360 * @return The user preferences file (preferences.xml) 361 */ 362 public File getPreferenceFile() { 363 return new File(getPreferencesDirectory(), "preferences.xml"); 364 } 365 366 /** 367 * Returns the cache file for default preferences. 368 * @return the cache file for default preferences 369 */ 370 public File getDefaultsCacheFile() { 371 return new File(getCacheDirectory(), "default_preferences.xml"); 372 } 373 374 /** 375 * Returns the user plugin directory. 376 * @return The user plugin directory 377 */ 378 public File getPluginsDirectory() { 379 return new File(getUserDataDirectory(), "plugins"); 380 } 381 382 /** 383 * Get the directory where cached content of any kind should be stored. 384 * 385 * If the directory doesn't exist on the file system, it will be created by this method. 386 * 387 * @return the cache directory 388 */ 389 public File getCacheDirectory() { 390 if (cacheDir != null) 391 return cacheDir; 392 String path = System.getProperty("josm.cache"); 393 if (path != null) { 394 cacheDir = new File(path).getAbsoluteFile(); 395 } else { 396 path = System.getProperty("josm.home"); 397 if (path != null) { 398 cacheDir = new File(path, "cache"); 399 } else { 400 path = get("cache.folder", null); 401 if (path != null) { 402 cacheDir = new File(path).getAbsoluteFile(); 403 } else { 404 cacheDir = Main.platform.getDefaultCacheDirectory(); 405 } 406 } 407 } 408 if (!cacheDir.exists() && !cacheDir.mkdirs()) { 409 Main.warn(tr("Failed to create missing cache directory: {0}", cacheDir.getAbsoluteFile())); 410 JOptionPane.showMessageDialog( 411 Main.parent, 412 tr("<html>Failed to create missing cache directory: {0}</html>", cacheDir.getAbsoluteFile()), 413 tr("Error"), 414 JOptionPane.ERROR_MESSAGE 415 ); 416 } 417 return cacheDir; 418 } 419 420 private static void addPossibleResourceDir(Set<String> locations, String s) { 421 if (s != null) { 422 if (!s.endsWith(File.separator)) { 423 s += File.separator; 424 } 425 locations.add(s); 426 } 427 } 428 429 /** 430 * Returns a set of all existing directories where resources could be stored. 431 * @return A set of all existing directories where resources could be stored. 432 */ 433 public Collection<String> getAllPossiblePreferenceDirs() { 434 Set<String> locations = new HashSet<>(); 435 addPossibleResourceDir(locations, getPreferencesDirectory().getPath()); 436 addPossibleResourceDir(locations, getUserDataDirectory().getPath()); 437 addPossibleResourceDir(locations, System.getenv("JOSM_RESOURCES")); 438 addPossibleResourceDir(locations, System.getProperty("josm.resources")); 439 if (Main.isPlatformWindows()) { 440 String appdata = System.getenv("APPDATA"); 441 if (System.getenv("ALLUSERSPROFILE") != null && appdata != null 442 && appdata.lastIndexOf(File.separator) != -1) { 443 appdata = appdata.substring(appdata.lastIndexOf(File.separator)); 444 locations.add(new File(new File(System.getenv("ALLUSERSPROFILE"), 445 appdata), "JOSM").getPath()); 446 } 447 } else { 448 locations.add("/usr/local/share/josm/"); 449 locations.add("/usr/local/lib/josm/"); 450 locations.add("/usr/share/josm/"); 451 locations.add("/usr/lib/josm/"); 452 } 453 return locations; 454 } 455 456 /** 457 * Get settings value for a certain key. 458 * @param key the identifier for the setting 459 * @return "" if there is nothing set for the preference key, the corresponding value otherwise. The result is not null. 460 */ 461 public synchronized String get(final String key) { 462 String value = get(key, null); 463 return value == null ? "" : value; 464 } 465 466 /** 467 * Get settings value for a certain key and provide default a value. 468 * @param key the identifier for the setting 469 * @param def the default value. For each call of get() with a given key, the default value must be the same. 470 * @return the corresponding value if the property has been set before, {@code def} otherwise 471 */ 472 public synchronized String get(final String key, final String def) { 473 return getSetting(key, new StringSetting(def), StringSetting.class).getValue(); 474 } 475 476 public synchronized Map<String, String> getAllPrefix(final String prefix) { 477 final Map<String, String> all = new TreeMap<>(); 478 for (final Entry<String, Setting<?>> e : settingsMap.entrySet()) { 479 if (e.getKey().startsWith(prefix) && (e.getValue() instanceof StringSetting)) { 480 all.put(e.getKey(), ((StringSetting) e.getValue()).getValue()); 481 } 482 } 483 return all; 484 } 485 486 public synchronized List<String> getAllPrefixCollectionKeys(final String prefix) { 487 final List<String> all = new LinkedList<>(); 488 for (Map.Entry<String, Setting<?>> entry : settingsMap.entrySet()) { 489 if (entry.getKey().startsWith(prefix) && entry.getValue() instanceof ListSetting) { 490 all.add(entry.getKey()); 491 } 492 } 493 return all; 494 } 495 496 public synchronized Map<String, String> getAllColors() { 497 final Map<String, String> all = new TreeMap<>(); 498 for (final Entry<String, Setting<?>> e : defaultsMap.entrySet()) { 499 if (e.getKey().startsWith("color.") && e.getValue() instanceof StringSetting) { 500 StringSetting d = (StringSetting) e.getValue(); 501 if (d.getValue() != null) { 502 all.put(e.getKey().substring(6), d.getValue()); 503 } 504 } 505 } 506 for (final Entry<String, Setting<?>> e : settingsMap.entrySet()) { 507 if (e.getKey().startsWith("color.") && (e.getValue() instanceof StringSetting)) { 508 all.put(e.getKey().substring(6), ((StringSetting) e.getValue()).getValue()); 509 } 510 } 511 return all; 512 } 513 514 public synchronized boolean getBoolean(final String key) { 515 String s = get(key, null); 516 return s != null && Boolean.parseBoolean(s); 517 } 518 519 public synchronized boolean getBoolean(final String key, final boolean def) { 520 return Boolean.parseBoolean(get(key, Boolean.toString(def))); 521 } 522 523 public synchronized boolean getBoolean(final String key, final String specName, final boolean def) { 524 boolean generic = getBoolean(key, def); 525 String skey = key+'.'+specName; 526 Setting<?> prop = settingsMap.get(skey); 527 if (prop instanceof StringSetting) 528 return Boolean.parseBoolean(((StringSetting) prop).getValue()); 529 else 530 return generic; 531 } 532 533 /** 534 * Set a value for a certain setting. 535 * @param key the unique identifier for the setting 536 * @param value the value of the setting. Can be null or "" which both removes the key-value entry. 537 * @return {@code true}, if something has changed (i.e. value is different than before) 538 */ 539 public boolean put(final String key, String value) { 540 return putSetting(key, value == null || value.isEmpty() ? null : new StringSetting(value)); 541 } 542 543 /** 544 * Set a boolean value for a certain setting. 545 * @param key the unique identifier for the setting 546 * @param value The new value 547 * @return {@code true}, if something has changed (i.e. value is different than before) 548 * @see BooleanProperty 549 */ 550 public boolean put(final String key, final boolean value) { 551 return put(key, Boolean.toString(value)); 552 } 553 554 /** 555 * Set a boolean value for a certain setting. 556 * @param key the unique identifier for the setting 557 * @param value The new value 558 * @return {@code true}, if something has changed (i.e. value is different than before) 559 * @see IntegerProperty 560 */ 561 public boolean putInteger(final String key, final Integer value) { 562 return put(key, Integer.toString(value)); 563 } 564 565 /** 566 * Set a boolean value for a certain setting. 567 * @param key the unique identifier for the setting 568 * @param value The new value 569 * @return {@code true}, if something has changed (i.e. value is different than before) 570 * @see DoubleProperty 571 */ 572 public boolean putDouble(final String key, final Double value) { 573 return put(key, Double.toString(value)); 574 } 575 576 /** 577 * Set a boolean value for a certain setting. 578 * @param key the unique identifier for the setting 579 * @param value The new value 580 * @return {@code true}, if something has changed (i.e. value is different than before) 581 * @see LongProperty 582 */ 583 public boolean putLong(final String key, final Long value) { 584 return put(key, Long.toString(value)); 585 } 586 587 /** 588 * Called after every put. In case of a problem, do nothing but output the error in log. 589 * @throws IOException if any I/O error occurs 590 */ 591 public synchronized void save() throws IOException { 592 save(getPreferenceFile(), settingsMap.entrySet().stream().filter(NO_DEFAULT_SETTINGS_ENTRY), false); 593 } 594 595 public synchronized void saveDefaults() throws IOException { 596 save(getDefaultsCacheFile(), defaultsMap.entrySet().stream(), true); 597 } 598 599 protected void save(File prefFile, Stream<Entry<String, Setting<?>>> settings, boolean defaults) throws IOException { 600 if (!defaults) { 601 /* currently unused, but may help to fix configuration issues in future */ 602 putInteger("josm.version", Version.getInstance().getVersion()); 603 604 updateSystemProperties(); 605 } 606 607 File backupFile = new File(prefFile + "_backup"); 608 609 // Backup old preferences if there are old preferences 610 if (prefFile.exists() && prefFile.length() > 0 && initSuccessful) { 611 Utils.copyFile(prefFile, backupFile); 612 } 613 614 try (PreferencesWriter writer = new PreferencesWriter( 615 new PrintWriter(new File(prefFile + "_tmp"), StandardCharsets.UTF_8.name()), false, defaults)) { 616 writer.write(settings); 617 } 618 619 File tmpFile = new File(prefFile + "_tmp"); 620 Utils.copyFile(tmpFile, prefFile); 621 Utils.deleteFile(tmpFile, marktr("Unable to delete temporary file {0}")); 622 623 setCorrectPermissions(prefFile); 624 setCorrectPermissions(backupFile); 625 } 626 627 private static void setCorrectPermissions(File file) { 628 if (!file.setReadable(false, false) && Main.isDebugEnabled()) { 629 Main.debug(tr("Unable to set file non-readable {0}", file.getAbsolutePath())); 630 } 631 if (!file.setWritable(false, false) && Main.isDebugEnabled()) { 632 Main.debug(tr("Unable to set file non-writable {0}", file.getAbsolutePath())); 633 } 634 if (!file.setExecutable(false, false) && Main.isDebugEnabled()) { 635 Main.debug(tr("Unable to set file non-executable {0}", file.getAbsolutePath())); 636 } 637 if (!file.setReadable(true, true) && Main.isDebugEnabled()) { 638 Main.debug(tr("Unable to set file readable {0}", file.getAbsolutePath())); 639 } 640 if (!file.setWritable(true, true) && Main.isDebugEnabled()) { 641 Main.debug(tr("Unable to set file writable {0}", file.getAbsolutePath())); 642 } 643 } 644 645 /** 646 * Loads preferences from settings file. 647 * @throws IOException if any I/O error occurs while reading the file 648 * @throws SAXException if the settings file does not contain valid XML 649 * @throws XMLStreamException if an XML error occurs while parsing the file (after validation) 650 */ 651 protected void load() throws IOException, SAXException, XMLStreamException { 652 File pref = getPreferenceFile(); 653 PreferencesReader.validateXML(pref); 654 PreferencesReader reader = new PreferencesReader(pref, false); 655 reader.parse(); 656 settingsMap.clear(); 657 settingsMap.putAll(reader.getSettings()); 658 updateSystemProperties(); 659 removeObsolete(reader.getVersion()); 660 } 661 662 /** 663 * Loads default preferences from default settings cache file. 664 * 665 * Discards entries older than {@link #MAX_AGE_DEFAULT_PREFERENCES}. 666 * 667 * @throws IOException if any I/O error occurs while reading the file 668 * @throws SAXException if the settings file does not contain valid XML 669 * @throws XMLStreamException if an XML error occurs while parsing the file (after validation) 670 */ 671 protected void loadDefaults() throws IOException, XMLStreamException, SAXException { 672 File def = getDefaultsCacheFile(); 673 PreferencesReader.validateXML(def); 674 PreferencesReader reader = new PreferencesReader(def, true); 675 reader.parse(); 676 defaultsMap.clear(); 677 long minTime = System.currentTimeMillis() / 1000 - MAX_AGE_DEFAULT_PREFERENCES; 678 for (Entry<String, Setting<?>> e : reader.getSettings().entrySet()) { 679 if (e.getValue().getTime() >= minTime) { 680 defaultsMap.put(e.getKey(), e.getValue()); 681 } 682 } 683 } 684 685 /** 686 * Loads preferences from XML reader. 687 * @param in XML reader 688 * @throws XMLStreamException if any XML stream error occurs 689 * @throws IOException if any I/O error occurs 690 */ 691 public void fromXML(Reader in) throws XMLStreamException, IOException { 692 PreferencesReader reader = new PreferencesReader(in, false); 693 reader.parse(); 694 settingsMap.clear(); 695 settingsMap.putAll(reader.getSettings()); 696 } 697 698 /** 699 * Initializes preferences. 700 * @param reset if {@code true}, current settings file is replaced by the default one 701 */ 702 public void init(boolean reset) { 703 initSuccessful = false; 704 // get the preferences. 705 File prefDir = getPreferencesDirectory(); 706 if (prefDir.exists()) { 707 if (!prefDir.isDirectory()) { 708 Main.warn(tr("Failed to initialize preferences. Preference directory ''{0}'' is not a directory.", 709 prefDir.getAbsoluteFile())); 710 JOptionPane.showMessageDialog( 711 Main.parent, 712 tr("<html>Failed to initialize preferences.<br>Preference directory ''{0}'' is not a directory.</html>", 713 prefDir.getAbsoluteFile()), 714 tr("Error"), 715 JOptionPane.ERROR_MESSAGE 716 ); 717 return; 718 } 719 } else { 720 if (!prefDir.mkdirs()) { 721 Main.warn(tr("Failed to initialize preferences. Failed to create missing preference directory: {0}", 722 prefDir.getAbsoluteFile())); 723 JOptionPane.showMessageDialog( 724 Main.parent, 725 tr("<html>Failed to initialize preferences.<br>Failed to create missing preference directory: {0}</html>", 726 prefDir.getAbsoluteFile()), 727 tr("Error"), 728 JOptionPane.ERROR_MESSAGE 729 ); 730 return; 731 } 732 } 733 734 File preferenceFile = getPreferenceFile(); 735 try { 736 if (!preferenceFile.exists()) { 737 Main.info(tr("Missing preference file ''{0}''. Creating a default preference file.", preferenceFile.getAbsoluteFile())); 738 resetToDefault(); 739 save(); 740 } else if (reset) { 741 File backupFile = new File(prefDir, "preferences.xml.bak"); 742 Main.platform.rename(preferenceFile, backupFile); 743 Main.warn(tr("Replacing existing preference file ''{0}'' with default preference file.", preferenceFile.getAbsoluteFile())); 744 resetToDefault(); 745 save(); 746 } 747 } catch (IOException e) { 748 Main.error(e); 749 JOptionPane.showMessageDialog( 750 Main.parent, 751 tr("<html>Failed to initialize preferences.<br>Failed to reset preference file to default: {0}</html>", 752 getPreferenceFile().getAbsoluteFile()), 753 tr("Error"), 754 JOptionPane.ERROR_MESSAGE 755 ); 756 return; 757 } 758 try { 759 load(); 760 initSuccessful = true; 761 } catch (IOException | SAXException | XMLStreamException e) { 762 Main.error(e); 763 File backupFile = new File(prefDir, "preferences.xml.bak"); 764 JOptionPane.showMessageDialog( 765 Main.parent, 766 tr("<html>Preferences file had errors.<br> Making backup of old one to <br>{0}<br> " + 767 "and creating a new default preference file.</html>", 768 backupFile.getAbsoluteFile()), 769 tr("Error"), 770 JOptionPane.ERROR_MESSAGE 771 ); 772 Main.platform.rename(preferenceFile, backupFile); 773 try { 774 resetToDefault(); 775 save(); 776 } catch (IOException e1) { 777 Main.error(e1); 778 Main.warn(tr("Failed to initialize preferences. Failed to reset preference file to default: {0}", getPreferenceFile())); 779 } 780 } 781 File def = getDefaultsCacheFile(); 782 if (def.exists()) { 783 try { 784 loadDefaults(); 785 } catch (IOException | XMLStreamException | SAXException e) { 786 Main.error(e); 787 Main.warn(tr("Failed to load defaults cache file: {0}", def)); 788 defaultsMap.clear(); 789 if (!def.delete()) { 790 Main.warn(tr("Failed to delete faulty defaults cache file: {0}", def)); 791 } 792 } 793 } 794 } 795 796 /** 797 * Resets the preferences to their initial state. This resets all values and file associations. 798 * The default values and listeners are not removed. 799 * <p> 800 * It is meant to be called before {@link #init(boolean)} 801 * @since 10876 802 */ 803 public void resetToInitialState() { 804 resetToDefault(); 805 preferencesDir = null; 806 cacheDir = null; 807 userdataDir = null; 808 saveOnPut = true; 809 initSuccessful = false; 810 } 811 812 /** 813 * Reset all values stored in this map to the default values. This clears the preferences. 814 */ 815 public final void resetToDefault() { 816 settingsMap.clear(); 817 } 818 819 /** 820 * Convenience method for accessing colour preferences. 821 * <p> 822 * To be removed: end of 2016 823 * 824 * @param colName name of the colour 825 * @param def default value 826 * @return a Color object for the configured colour, or the default value if none configured. 827 * @deprecated Use a {@link ColorProperty} instead. 828 */ 829 @Deprecated 830 public synchronized Color getColor(String colName, Color def) { 831 return getColor(colName, null, def); 832 } 833 834 /* only for preferences */ 835 public synchronized String getColorName(String o) { 836 Matcher m = Pattern.compile("mappaint\\.(.+?)\\.(.+)").matcher(o); 837 if (m.matches()) { 838 return tr("Paint style {0}: {1}", tr(I18n.escape(m.group(1))), tr(I18n.escape(m.group(2)))); 839 } 840 m = Pattern.compile("layer (.+)").matcher(o); 841 if (m.matches()) { 842 return tr("Layer: {0}", tr(I18n.escape(m.group(1)))); 843 } 844 return tr(I18n.escape(colornames.containsKey(o) ? colornames.get(o) : o)); 845 } 846 847 /** 848 * Convenience method for accessing colour preferences. 849 * <p> 850 * To be removed: end of 2016 851 * @param colName name of the colour 852 * @param specName name of the special colour settings 853 * @param def default value 854 * @return a Color object for the configured colour, or the default value if none configured. 855 * @deprecated Use a {@link ColorProperty} instead. 856 * You can replace this by: <code>new ColorProperty(colName, def).getChildColor(specName)</code> 857 */ 858 @Deprecated 859 public synchronized Color getColor(String colName, String specName, Color def) { 860 String colKey = ColorProperty.getColorKey(colName); 861 registerColor(colKey, colName); 862 String colStr = specName != null ? get("color."+specName) : ""; 863 if (colStr.isEmpty()) { 864 colStr = get(colKey, ColorHelper.color2html(def, true)); 865 } 866 if (colStr != null && !colStr.isEmpty()) { 867 return ColorHelper.html2color(colStr); 868 } else { 869 return def; 870 } 871 } 872 873 /** 874 * Registers a color name conversion for the global color registry. 875 * @param colKey The key 876 * @param colName The name of the color. 877 * @since 10824 878 */ 879 public void registerColor(String colKey, String colName) { 880 if (!colKey.equals(colName)) { 881 colornames.put(colKey, colName); 882 } 883 } 884 885 public synchronized Color getDefaultColor(String colKey) { 886 StringSetting col = Utils.cast(defaultsMap.get("color."+colKey), StringSetting.class); 887 String colStr = col == null ? null : col.getValue(); 888 return colStr == null || colStr.isEmpty() ? null : ColorHelper.html2color(colStr); 889 } 890 891 public synchronized boolean putColor(String colKey, Color val) { 892 return put("color."+colKey, val != null ? ColorHelper.color2html(val, true) : null); 893 } 894 895 public synchronized int getInteger(String key, int def) { 896 String v = get(key, Integer.toString(def)); 897 if (v.isEmpty()) 898 return def; 899 900 try { 901 return Integer.parseInt(v); 902 } catch (NumberFormatException e) { 903 // fall out 904 Main.trace(e); 905 } 906 return def; 907 } 908 909 public synchronized int getInteger(String key, String specName, int def) { 910 String v = get(key+'.'+specName); 911 if (v.isEmpty()) 912 v = get(key, Integer.toString(def)); 913 if (v.isEmpty()) 914 return def; 915 916 try { 917 return Integer.parseInt(v); 918 } catch (NumberFormatException e) { 919 // fall out 920 Main.trace(e); 921 } 922 return def; 923 } 924 925 public synchronized long getLong(String key, long def) { 926 String v = get(key, Long.toString(def)); 927 if (null == v) 928 return def; 929 930 try { 931 return Long.parseLong(v); 932 } catch (NumberFormatException e) { 933 // fall out 934 Main.trace(e); 935 } 936 return def; 937 } 938 939 public synchronized double getDouble(String key, double def) { 940 String v = get(key, Double.toString(def)); 941 if (null == v) 942 return def; 943 944 try { 945 return Double.parseDouble(v); 946 } catch (NumberFormatException e) { 947 // fall out 948 Main.trace(e); 949 } 950 return def; 951 } 952 953 /** 954 * Get a list of values for a certain key 955 * @param key the identifier for the setting 956 * @param def the default value. 957 * @return the corresponding value if the property has been set before, {@code def} otherwise 958 */ 959 public Collection<String> getCollection(String key, Collection<String> def) { 960 return getSetting(key, ListSetting.create(def), ListSetting.class).getValue(); 961 } 962 963 /** 964 * Get a list of values for a certain key 965 * @param key the identifier for the setting 966 * @return the corresponding value if the property has been set before, an empty collection otherwise. 967 */ 968 public Collection<String> getCollection(String key) { 969 Collection<String> val = getCollection(key, null); 970 return val == null ? Collections.<String>emptyList() : val; 971 } 972 973 public synchronized void removeFromCollection(String key, String value) { 974 List<String> a = new ArrayList<>(getCollection(key, Collections.<String>emptyList())); 975 a.remove(value); 976 putCollection(key, a); 977 } 978 979 /** 980 * Set a value for a certain setting. The changed setting is saved to the preference file immediately. 981 * Due to caching mechanisms on modern operating systems and hardware, this shouldn't be a performance problem. 982 * @param key the unique identifier for the setting 983 * @param setting the value of the setting. In case it is null, the key-value entry will be removed. 984 * @return {@code true}, if something has changed (i.e. value is different than before) 985 */ 986 public boolean putSetting(final String key, Setting<?> setting) { 987 CheckParameterUtil.ensureParameterNotNull(key); 988 if (setting != null && setting.getValue() == null) 989 throw new IllegalArgumentException("setting argument must not have null value"); 990 Setting<?> settingOld; 991 Setting<?> settingCopy = null; 992 synchronized (this) { 993 if (setting == null) { 994 settingOld = settingsMap.remove(key); 995 if (settingOld == null) 996 return false; 997 } else { 998 settingOld = settingsMap.get(key); 999 if (setting.equals(settingOld)) 1000 return false; 1001 if (settingOld == null && setting.equals(defaultsMap.get(key))) 1002 return false; 1003 settingCopy = setting.copy(); 1004 settingsMap.put(key, settingCopy); 1005 } 1006 if (saveOnPut) { 1007 try { 1008 save(); 1009 } catch (IOException e) { 1010 Main.warn(e, tr("Failed to persist preferences to ''{0}''", getPreferenceFile().getAbsoluteFile())); 1011 } 1012 } 1013 } 1014 // Call outside of synchronized section in case some listener wait for other thread that wait for preference lock 1015 firePreferenceChanged(key, settingOld, settingCopy); 1016 return true; 1017 } 1018 1019 public synchronized Setting<?> getSetting(String key, Setting<?> def) { 1020 return getSetting(key, def, Setting.class); 1021 } 1022 1023 /** 1024 * Get settings value for a certain key and provide default a value. 1025 * @param <T> the setting type 1026 * @param key the identifier for the setting 1027 * @param def the default value. For each call of getSetting() with a given key, the default value must be the same. 1028 * <code>def</code> must not be null, but the value of <code>def</code> can be null. 1029 * @param klass the setting type (same as T) 1030 * @return the corresponding value if the property has been set before, {@code def} otherwise 1031 */ 1032 @SuppressWarnings("unchecked") 1033 public synchronized <T extends Setting<?>> T getSetting(String key, T def, Class<T> klass) { 1034 CheckParameterUtil.ensureParameterNotNull(key); 1035 CheckParameterUtil.ensureParameterNotNull(def); 1036 Setting<?> oldDef = defaultsMap.get(key); 1037 if (oldDef != null && oldDef.isNew() && oldDef.getValue() != null && def.getValue() != null && !def.equals(oldDef)) { 1038 Main.info("Defaults for " + key + " differ: " + def + " != " + defaultsMap.get(key)); 1039 } 1040 if (def.getValue() != null || oldDef == null) { 1041 Setting<?> defCopy = def.copy(); 1042 defCopy.setTime(System.currentTimeMillis() / 1000); 1043 defCopy.setNew(true); 1044 defaultsMap.put(key, defCopy); 1045 } 1046 Setting<?> prop = settingsMap.get(key); 1047 if (klass.isInstance(prop)) { 1048 return (T) prop; 1049 } else { 1050 return def; 1051 } 1052 } 1053 1054 /** 1055 * Put a collection. 1056 * @param key key 1057 * @param value value 1058 * @return {@code true}, if something has changed (i.e. value is different than before) 1059 */ 1060 public boolean putCollection(String key, Collection<String> value) { 1061 return putSetting(key, value == null ? null : ListSetting.create(value)); 1062 } 1063 1064 /** 1065 * Saves at most {@code maxsize} items of collection {@code val}. 1066 * @param key key 1067 * @param maxsize max number of items to save 1068 * @param val value 1069 * @return {@code true}, if something has changed (i.e. value is different than before) 1070 */ 1071 public boolean putCollectionBounded(String key, int maxsize, Collection<String> val) { 1072 Collection<String> newCollection = new ArrayList<>(Math.min(maxsize, val.size())); 1073 for (String i : val) { 1074 if (newCollection.size() >= maxsize) { 1075 break; 1076 } 1077 newCollection.add(i); 1078 } 1079 return putCollection(key, newCollection); 1080 } 1081 1082 /** 1083 * Used to read a 2-dimensional array of strings from the preference file. 1084 * If not a single entry could be found, <code>def</code> is returned. 1085 * @param key preference key 1086 * @param def default array value 1087 * @return array value 1088 */ 1089 @SuppressWarnings({ "unchecked", "rawtypes" }) 1090 public synchronized Collection<Collection<String>> getArray(String key, Collection<Collection<String>> def) { 1091 ListListSetting val = getSetting(key, ListListSetting.create(def), ListListSetting.class); 1092 return (Collection) val.getValue(); 1093 } 1094 1095 public Collection<Collection<String>> getArray(String key) { 1096 Collection<Collection<String>> res = getArray(key, null); 1097 return res == null ? Collections.<Collection<String>>emptyList() : res; 1098 } 1099 1100 /** 1101 * Put an array. 1102 * @param key key 1103 * @param value value 1104 * @return {@code true}, if something has changed (i.e. value is different than before) 1105 */ 1106 public boolean putArray(String key, Collection<Collection<String>> value) { 1107 return putSetting(key, value == null ? null : ListListSetting.create(value)); 1108 } 1109 1110 public Collection<Map<String, String>> getListOfStructs(String key, Collection<Map<String, String>> def) { 1111 return getSetting(key, new MapListSetting(def == null ? null : new ArrayList<>(def)), MapListSetting.class).getValue(); 1112 } 1113 1114 public boolean putListOfStructs(String key, Collection<Map<String, String>> value) { 1115 return putSetting(key, value == null ? null : new MapListSetting(new ArrayList<>(value))); 1116 } 1117 1118 /** 1119 * Annotation used for converting objects to String Maps and vice versa. 1120 * Indicates that a certain field should be considered in the conversion process. Otherwise it is ignored. 1121 * 1122 * @see #serializeStruct(java.lang.Object, java.lang.Class) 1123 * @see #deserializeStruct(java.util.Map, java.lang.Class) 1124 */ 1125 @Retention(RetentionPolicy.RUNTIME) // keep annotation at runtime 1126 public @interface pref { } 1127 1128 /** 1129 * Annotation used for converting objects to String Maps. 1130 * Indicates that a certain field should be written to the map, even if the value is the same as the default value. 1131 * 1132 * @see #serializeStruct(java.lang.Object, java.lang.Class) 1133 */ 1134 @Retention(RetentionPolicy.RUNTIME) // keep annotation at runtime 1135 public @interface writeExplicitly { } 1136 1137 /** 1138 * Get a list of hashes which are represented by a struct-like class. 1139 * Possible properties are given by fields of the class klass that have the @pref annotation. 1140 * Default constructor is used to initialize the struct objects, properties then override some of these default values. 1141 * @param <T> klass type 1142 * @param key main preference key 1143 * @param klass The struct class 1144 * @return a list of objects of type T or an empty list if nothing was found 1145 */ 1146 public <T> List<T> getListOfStructs(String key, Class<T> klass) { 1147 List<T> r = getListOfStructs(key, null, klass); 1148 if (r == null) 1149 return Collections.emptyList(); 1150 else 1151 return r; 1152 } 1153 1154 /** 1155 * same as above, but returns def if nothing was found 1156 * @param <T> klass type 1157 * @param key main preference key 1158 * @param def default value 1159 * @param klass The struct class 1160 * @return a list of objects of type T or {@code def} if nothing was found 1161 */ 1162 public <T> List<T> getListOfStructs(String key, Collection<T> def, Class<T> klass) { 1163 Collection<Map<String, String>> prop = 1164 getListOfStructs(key, def == null ? null : serializeListOfStructs(def, klass)); 1165 if (prop == null) 1166 return def == null ? null : new ArrayList<>(def); 1167 List<T> lst = new ArrayList<>(); 1168 for (Map<String, String> entries : prop) { 1169 T struct = deserializeStruct(entries, klass); 1170 lst.add(struct); 1171 } 1172 return lst; 1173 } 1174 1175 /** 1176 * Convenience method that saves a MapListSetting which is provided as a collection of objects. 1177 * 1178 * Each object is converted to a <code>Map<String, String></code> using the fields with {@link pref} annotation. 1179 * The field name is the key and the value will be converted to a string. 1180 * 1181 * Considers only fields that have the @pref annotation. 1182 * In addition it does not write fields with null values. (Thus they are cleared) 1183 * Default values are given by the field values after default constructor has been called. 1184 * Fields equal to the default value are not written unless the field has the @writeExplicitly annotation. 1185 * @param <T> the class, 1186 * @param key main preference key 1187 * @param val the list that is supposed to be saved 1188 * @param klass The struct class 1189 * @return true if something has changed 1190 */ 1191 public <T> boolean putListOfStructs(String key, Collection<T> val, Class<T> klass) { 1192 return putListOfStructs(key, serializeListOfStructs(val, klass)); 1193 } 1194 1195 private static <T> Collection<Map<String, String>> serializeListOfStructs(Collection<T> l, Class<T> klass) { 1196 if (l == null) 1197 return null; 1198 Collection<Map<String, String>> vals = new ArrayList<>(); 1199 for (T struct : l) { 1200 if (struct == null) { 1201 continue; 1202 } 1203 vals.add(serializeStruct(struct, klass)); 1204 } 1205 return vals; 1206 } 1207 1208 @SuppressWarnings("rawtypes") 1209 private static String mapToJson(Map map) { 1210 StringWriter stringWriter = new StringWriter(); 1211 try (JsonWriter writer = Json.createWriter(stringWriter)) { 1212 JsonObjectBuilder object = Json.createObjectBuilder(); 1213 for (Object o: map.entrySet()) { 1214 Entry e = (Entry) o; 1215 Object evalue = e.getValue(); 1216 object.add(e.getKey().toString(), evalue.toString()); 1217 } 1218 writer.writeObject(object.build()); 1219 } 1220 return stringWriter.toString(); 1221 } 1222 1223 @SuppressWarnings({ "rawtypes", "unchecked" }) 1224 private static Map mapFromJson(String s) { 1225 Map ret = null; 1226 try (JsonReader reader = Json.createReader(new StringReader(s))) { 1227 JsonObject object = reader.readObject(); 1228 ret = new HashMap(object.size()); 1229 for (Entry<String, JsonValue> e: object.entrySet()) { 1230 JsonValue value = e.getValue(); 1231 if (value instanceof JsonString) { 1232 // in some cases, when JsonValue.toString() is called, then additional quotation marks are left in value 1233 ret.put(e.getKey(), ((JsonString) value).getString()); 1234 } else { 1235 ret.put(e.getKey(), e.getValue().toString()); 1236 } 1237 } 1238 } 1239 return ret; 1240 } 1241 1242 @SuppressWarnings("rawtypes") 1243 private static String multiMapToJson(MultiMap map) { 1244 StringWriter stringWriter = new StringWriter(); 1245 try (JsonWriter writer = Json.createWriter(stringWriter)) { 1246 JsonObjectBuilder object = Json.createObjectBuilder(); 1247 for (Object o: map.entrySet()) { 1248 Entry e = (Entry) o; 1249 Set evalue = (Set) e.getValue(); 1250 JsonArrayBuilder a = Json.createArrayBuilder(); 1251 for (Object evo: evalue) { 1252 a.add(evo.toString()); 1253 } 1254 object.add(e.getKey().toString(), a.build()); 1255 } 1256 writer.writeObject(object.build()); 1257 } 1258 return stringWriter.toString(); 1259 } 1260 1261 @SuppressWarnings({ "rawtypes", "unchecked" }) 1262 private static MultiMap multiMapFromJson(String s) { 1263 MultiMap ret = null; 1264 try (JsonReader reader = Json.createReader(new StringReader(s))) { 1265 JsonObject object = reader.readObject(); 1266 ret = new MultiMap(object.size()); 1267 for (Entry<String, JsonValue> e: object.entrySet()) { 1268 JsonValue value = e.getValue(); 1269 if (value instanceof JsonArray) { 1270 for (JsonString js: ((JsonArray) value).getValuesAs(JsonString.class)) { 1271 ret.put(e.getKey(), js.getString()); 1272 } 1273 } else if (value instanceof JsonString) { 1274 // in some cases, when JsonValue.toString() is called, then additional quotation marks are left in value 1275 ret.put(e.getKey(), ((JsonString) value).getString()); 1276 } else { 1277 ret.put(e.getKey(), e.getValue().toString()); 1278 } 1279 } 1280 } 1281 return ret; 1282 } 1283 1284 /** 1285 * Convert an object to a String Map, by using field names and values as map key and value. 1286 * 1287 * The field value is converted to a String. 1288 * 1289 * Only fields with annotation {@link pref} are taken into account. 1290 * 1291 * Fields will not be written to the map if the value is null or unchanged 1292 * (compared to an object created with the no-arg-constructor). 1293 * The {@link writeExplicitly} annotation overrides this behavior, i.e. the default value will also be written. 1294 * 1295 * @param <T> the class of the object <code>struct</code> 1296 * @param struct the object to be converted 1297 * @param klass the class T 1298 * @return the resulting map (same data content as <code>struct</code>) 1299 */ 1300 public static <T> Map<String, String> serializeStruct(T struct, Class<T> klass) { 1301 T structPrototype; 1302 try { 1303 structPrototype = klass.getConstructor().newInstance(); 1304 } catch (ReflectiveOperationException ex) { 1305 throw new IllegalArgumentException(ex); 1306 } 1307 1308 Map<String, String> hash = new LinkedHashMap<>(); 1309 for (Field f : klass.getDeclaredFields()) { 1310 if (f.getAnnotation(pref.class) == null) { 1311 continue; 1312 } 1313 Utils.setObjectsAccessible(f); 1314 try { 1315 Object fieldValue = f.get(struct); 1316 Object defaultFieldValue = f.get(structPrototype); 1317 if (fieldValue != null && (f.getAnnotation(writeExplicitly.class) != null || !Objects.equals(fieldValue, defaultFieldValue))) { 1318 String key = f.getName().replace('_', '-'); 1319 if (fieldValue instanceof Map) { 1320 hash.put(key, mapToJson((Map<?, ?>) fieldValue)); 1321 } else if (fieldValue instanceof MultiMap) { 1322 hash.put(key, multiMapToJson((MultiMap<?, ?>) fieldValue)); 1323 } else { 1324 hash.put(key, fieldValue.toString()); 1325 } 1326 } 1327 } catch (IllegalAccessException ex) { 1328 throw new RuntimeException(ex); 1329 } 1330 } 1331 return hash; 1332 } 1333 1334 /** 1335 * Converts a String-Map to an object of a certain class, by comparing map keys to field names of the class and assigning 1336 * map values to the corresponding fields. 1337 * 1338 * The map value (a String) is converted to the field type. Supported types are: boolean, Boolean, int, Integer, double, 1339 * Double, String, Map<String, String> and Map<String, List<String>>. 1340 * 1341 * Only fields with annotation {@link pref} are taken into account. 1342 * @param <T> the class 1343 * @param hash the string map with initial values 1344 * @param klass the class T 1345 * @return an object of class T, initialized as described above 1346 */ 1347 public static <T> T deserializeStruct(Map<String, String> hash, Class<T> klass) { 1348 T struct = null; 1349 try { 1350 struct = klass.getConstructor().newInstance(); 1351 } catch (ReflectiveOperationException ex) { 1352 throw new IllegalArgumentException(ex); 1353 } 1354 for (Entry<String, String> key_value : hash.entrySet()) { 1355 Object value; 1356 Field f; 1357 try { 1358 f = klass.getDeclaredField(key_value.getKey().replace('-', '_')); 1359 } catch (NoSuchFieldException ex) { 1360 Main.trace(ex); 1361 continue; 1362 } 1363 if (f.getAnnotation(pref.class) == null) { 1364 continue; 1365 } 1366 Utils.setObjectsAccessible(f); 1367 if (f.getType() == Boolean.class || f.getType() == boolean.class) { 1368 value = Boolean.valueOf(key_value.getValue()); 1369 } else if (f.getType() == Integer.class || f.getType() == int.class) { 1370 try { 1371 value = Integer.valueOf(key_value.getValue()); 1372 } catch (NumberFormatException nfe) { 1373 continue; 1374 } 1375 } else if (f.getType() == Double.class || f.getType() == double.class) { 1376 try { 1377 value = Double.valueOf(key_value.getValue()); 1378 } catch (NumberFormatException nfe) { 1379 continue; 1380 } 1381 } else if (f.getType() == String.class) { 1382 value = key_value.getValue(); 1383 } else if (f.getType().isAssignableFrom(Map.class)) { 1384 value = mapFromJson(key_value.getValue()); 1385 } else if (f.getType().isAssignableFrom(MultiMap.class)) { 1386 value = multiMapFromJson(key_value.getValue()); 1387 } else 1388 throw new RuntimeException("unsupported preference primitive type"); 1389 1390 try { 1391 f.set(struct, value); 1392 } catch (IllegalArgumentException ex) { 1393 throw new AssertionError(ex); 1394 } catch (IllegalAccessException ex) { 1395 throw new RuntimeException(ex); 1396 } 1397 } 1398 return struct; 1399 } 1400 1401 public Map<String, Setting<?>> getAllSettings() { 1402 return new TreeMap<>(settingsMap); 1403 } 1404 1405 public Map<String, Setting<?>> getAllDefaults() { 1406 return new TreeMap<>(defaultsMap); 1407 } 1408 1409 /** 1410 * Updates system properties with the current values in the preferences. 1411 * 1412 */ 1413 public void updateSystemProperties() { 1414 if ("true".equals(get("prefer.ipv6", "auto")) && !"true".equals(Utils.updateSystemProperty("java.net.preferIPv6Addresses", "true"))) { 1415 // never set this to false, only true! 1416 Main.info(tr("Try enabling IPv6 network, prefering IPv6 over IPv4 (only works on early startup).")); 1417 } 1418 Utils.updateSystemProperty("http.agent", Version.getInstance().getAgentString()); 1419 Utils.updateSystemProperty("user.language", get("language")); 1420 // Workaround to fix a Java bug. This ugly hack comes from Sun bug database: https://bugs.openjdk.java.net/browse/JDK-6292739 1421 // Force AWT toolkit to update its internal preferences (fix #6345). 1422 if (!GraphicsEnvironment.isHeadless()) { 1423 try { 1424 Field field = Toolkit.class.getDeclaredField("resources"); 1425 Utils.setObjectsAccessible(field); 1426 field.set(null, ResourceBundle.getBundle("sun.awt.resources.awt")); 1427 } catch (ReflectiveOperationException | MissingResourceException e) { 1428 Main.warn(e); 1429 } 1430 } 1431 // Possibility to disable SNI (not by default) in case of misconfigured https servers 1432 // See #9875 + http://stackoverflow.com/a/14884941/2257172 1433 // then https://josm.openstreetmap.de/ticket/12152#comment:5 for details 1434 if (getBoolean("jdk.tls.disableSNIExtension", false)) { 1435 Utils.updateSystemProperty("jsse.enableSNIExtension", "false"); 1436 } 1437 } 1438 1439 /** 1440 * Replies the collection of plugin site URLs from where plugin lists can be downloaded. 1441 * @return the collection of plugin site URLs 1442 * @see #getOnlinePluginSites 1443 */ 1444 public Collection<String> getPluginSites() { 1445 return getCollection("pluginmanager.sites", Collections.singleton(Main.getJOSMWebsite()+"/pluginicons%<?plugins=>")); 1446 } 1447 1448 /** 1449 * Returns the list of plugin sites available according to offline mode settings. 1450 * @return the list of available plugin sites 1451 * @since 8471 1452 */ 1453 public Collection<String> getOnlinePluginSites() { 1454 Collection<String> pluginSites = new ArrayList<>(getPluginSites()); 1455 for (Iterator<String> it = pluginSites.iterator(); it.hasNext();) { 1456 try { 1457 OnlineResource.JOSM_WEBSITE.checkOfflineAccess(it.next(), Main.getJOSMWebsite()); 1458 } catch (OfflineAccessException ex) { 1459 Main.warn(ex, false); 1460 it.remove(); 1461 } 1462 } 1463 return pluginSites; 1464 } 1465 1466 /** 1467 * Sets the collection of plugin site URLs. 1468 * 1469 * @param sites the site URLs 1470 */ 1471 public void setPluginSites(Collection<String> sites) { 1472 putCollection("pluginmanager.sites", sites); 1473 } 1474 1475 /** 1476 * Returns XML describing these preferences. 1477 * @param nopass if password must be excluded 1478 * @return XML 1479 */ 1480 public String toXML(boolean nopass) { 1481 return toXML(settingsMap.entrySet(), nopass, false); 1482 } 1483 1484 /** 1485 * Returns XML describing the given preferences. 1486 * @param settings preferences settings 1487 * @param nopass if password must be excluded 1488 * @param defaults true, if default values are converted to XML, false for 1489 * regular preferences 1490 * @return XML 1491 */ 1492 public String toXML(Collection<Entry<String, Setting<?>>> settings, boolean nopass, boolean defaults) { 1493 try ( 1494 StringWriter sw = new StringWriter(); 1495 PreferencesWriter prefWriter = new PreferencesWriter(new PrintWriter(sw), nopass, defaults) 1496 ) { 1497 prefWriter.write(settings); 1498 sw.flush(); 1499 return sw.toString(); 1500 } catch (IOException e) { 1501 Main.error(e); 1502 return null; 1503 } 1504 } 1505 1506 /** 1507 * Removes obsolete preference settings. If you throw out a once-used preference 1508 * setting, add it to the list here with an expiry date (written as comment). If you 1509 * see something with an expiry date in the past, remove it from the list. 1510 * @param loadedVersion JOSM version when the preferences file was written 1511 */ 1512 private void removeObsolete(int loadedVersion) { 1513 /* drop in October 2016 */ 1514 if (loadedVersion < 9715) { 1515 Setting<?> setting = settingsMap.get("imagery.entries"); 1516 if (setting instanceof MapListSetting) { 1517 List<Map<String, String>> l = new LinkedList<>(); 1518 boolean modified = false; 1519 for (Map<String, String> map: ((MapListSetting) setting).getValue()) { 1520 Map<String, String> newMap = new HashMap<>(); 1521 for (Entry<String, String> entry: map.entrySet()) { 1522 String value = entry.getValue(); 1523 if ("noTileHeaders".equals(entry.getKey())) { 1524 value = value.replaceFirst("\":(\".*\")\\}", "\":[$1]}"); 1525 if (!value.equals(entry.getValue())) { 1526 modified = true; 1527 } 1528 } 1529 newMap.put(entry.getKey(), value); 1530 } 1531 l.add(newMap); 1532 } 1533 if (modified) { 1534 putListOfStructs("imagery.entries", l); 1535 } 1536 } 1537 } 1538 // drop in November 2016 1539 removeUrlFromEntries(loadedVersion, 9965, 1540 "mappaint.style.entries", 1541 "josm.openstreetmap.de/josmfile?page=Styles/LegacyStandard"); 1542 // drop in December 2016 1543 removeUrlFromEntries(loadedVersion, 10063, 1544 "validator.org.openstreetmap.josm.data.validation.tests.MapCSSTagChecker.entries", 1545 "resource://data/validator/power.mapcss"); 1546 // drop in March 2017 1547 if (loadedVersion < 11058) { 1548 migrateOldColorKeys(); 1549 } 1550 1551 for (String key : OBSOLETE_PREF_KEYS) { 1552 if (settingsMap.containsKey(key)) { 1553 settingsMap.remove(key); 1554 Main.info(tr("Preference setting {0} has been removed since it is no longer used.", key)); 1555 } 1556 } 1557 } 1558 1559 private void migrateOldColorKeys() { 1560 settingsMap.keySet().stream() 1561 .filter(key -> key.startsWith("color.")) 1562 .flatMap(key -> { 1563 final String newKey = ColorProperty.getColorKey(key.substring("color.".length())); 1564 return key.equals(newKey) || settingsMap.containsKey(newKey) 1565 ? Stream.empty() 1566 : Stream.of(new AbstractMap.SimpleImmutableEntry<>(key, newKey)); 1567 }) 1568 .collect(Collectors.toList()) // to avoid ConcurrentModificationException 1569 .forEach(entry -> { 1570 final String oldKey = entry.getKey(); 1571 final String newKey = entry.getValue(); 1572 Main.info("Migrating old color key {0} => {1}", oldKey, newKey); 1573 put(newKey, get(oldKey)); 1574 put(oldKey, null); 1575 }); 1576 } 1577 1578 private void removeUrlFromEntries(int loadedVersion, int versionMax, String key, String urlPart) { 1579 if (loadedVersion < versionMax) { 1580 Setting<?> setting = settingsMap.get(key); 1581 if (setting instanceof MapListSetting) { 1582 List<Map<String, String>> l = new LinkedList<>(); 1583 boolean modified = false; 1584 for (Map<String, String> map: ((MapListSetting) setting).getValue()) { 1585 String url = map.get("url"); 1586 if (url != null && url.contains(urlPart)) { 1587 modified = true; 1588 } else { 1589 l.add(map); 1590 } 1591 } 1592 if (modified) { 1593 putListOfStructs(key, l); 1594 } 1595 } 1596 } 1597 } 1598 1599 /** 1600 * Enables or not the preferences file auto-save mechanism (save each time a setting is changed). 1601 * This behaviour is enabled by default. 1602 * @param enable if {@code true}, makes JOSM save preferences file each time a setting is changed 1603 * @since 7085 1604 */ 1605 public final void enableSaveOnPut(boolean enable) { 1606 synchronized (this) { 1607 saveOnPut = enable; 1608 } 1609 } 1610}