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