001 // License: GPL. Copyright 2007 by Immanuel Scholz and others 002 package org.openstreetmap.josm.data; 003 004 import static org.openstreetmap.josm.tools.I18n.tr; 005 006 import java.awt.Color; 007 import java.awt.Toolkit; 008 import java.io.BufferedReader; 009 import java.io.File; 010 import java.io.FileInputStream; 011 import java.io.FileOutputStream; 012 import java.io.IOException; 013 import java.io.InputStreamReader; 014 import java.io.OutputStreamWriter; 015 import java.io.PrintWriter; 016 import java.io.Reader; 017 import java.lang.annotation.Retention; 018 import java.lang.annotation.RetentionPolicy; 019 import java.lang.reflect.Field; 020 import java.nio.channels.FileChannel; 021 import java.util.ArrayList; 022 import java.util.Arrays; 023 import java.util.Collection; 024 import java.util.Collections; 025 import java.util.Iterator; 026 import java.util.LinkedHashMap; 027 import java.util.LinkedList; 028 import java.util.List; 029 import java.util.Map; 030 import java.util.Map.Entry; 031 import java.util.ResourceBundle; 032 import java.util.SortedMap; 033 import java.util.TreeMap; 034 import java.util.concurrent.CopyOnWriteArrayList; 035 import java.util.regex.Matcher; 036 import java.util.regex.Pattern; 037 038 import javax.swing.JOptionPane; 039 import javax.swing.UIManager; 040 import javax.xml.XMLConstants; 041 import javax.xml.stream.XMLInputFactory; 042 import javax.xml.stream.XMLStreamConstants; 043 import javax.xml.stream.XMLStreamException; 044 import javax.xml.stream.XMLStreamReader; 045 import javax.xml.transform.stream.StreamSource; 046 import javax.xml.validation.Schema; 047 import javax.xml.validation.SchemaFactory; 048 import javax.xml.validation.Validator; 049 050 import org.openstreetmap.josm.Main; 051 import org.openstreetmap.josm.data.preferences.ColorProperty; 052 import org.openstreetmap.josm.io.MirroredInputStream; 053 import org.openstreetmap.josm.io.XmlWriter; 054 import org.openstreetmap.josm.tools.ColorHelper; 055 import org.openstreetmap.josm.tools.Utils; 056 057 /** 058 * This class holds all preferences for JOSM. 059 * 060 * Other classes can register their beloved properties here. All properties will be 061 * saved upon set-access. 062 * 063 * Each property is a key=setting pair, where key is a String and setting can be one of 064 * 4 types: 065 * string, list, list of lists and list of maps. 066 * In addition, each key has a unique default value that is set when the value is first 067 * accessed using one of the get...() methods. You can use the same preference 068 * key in different parts of the code, but the default value must be the same 069 * everywhere. A default value of null means, the setting has been requested, but 070 * no default value was set. This is used in advanced preferences to present a list 071 * off all possible settings. 072 * 073 * At the moment, you cannot put the empty string for string properties. 074 * put(key, "") means, the property is removed. 075 * 076 * @author imi 077 */ 078 public class Preferences { 079 /** 080 * Internal storage for the preference directory. 081 * Do not access this variable directly! 082 * @see #getPreferencesDirFile() 083 */ 084 private File preferencesDirFile = null; 085 /** 086 * Internal storage for the cache directory. 087 */ 088 private File cacheDirFile = null; 089 090 /** 091 * Map the property name to strings. Does not contain null or "" values. 092 */ 093 protected final SortedMap<String, String> properties = new TreeMap<String, String>(); 094 /** Map of defaults, can contain null values */ 095 protected final SortedMap<String, String> defaults = new TreeMap<String, String>(); 096 protected final SortedMap<String, String> colornames = new TreeMap<String, String>(); 097 098 /** Mapping for list settings. Must not contain null values */ 099 protected final SortedMap<String, List<String>> collectionProperties = new TreeMap<String, List<String>>(); 100 /** Defaults, can contain null values */ 101 protected final SortedMap<String, List<String>> collectionDefaults = new TreeMap<String, List<String>>(); 102 103 protected final SortedMap<String, List<List<String>>> arrayProperties = new TreeMap<String, List<List<String>>>(); 104 protected final SortedMap<String, List<List<String>>> arrayDefaults = new TreeMap<String, List<List<String>>>(); 105 106 protected final SortedMap<String, List<Map<String,String>>> listOfStructsProperties = new TreeMap<String, List<Map<String,String>>>(); 107 protected final SortedMap<String, List<Map<String,String>>> listOfStructsDefaults = new TreeMap<String, List<Map<String,String>>>(); 108 109 /** 110 * Interface for a preference value 111 * 112 * @param <T> the data type for the value 113 */ 114 public interface Setting<T> { 115 /** 116 * Returns the value of this setting. 117 * 118 * @return the value of this setting 119 */ 120 T getValue(); 121 122 /** 123 * Enable usage of the visitor pattern. 124 * 125 * @param visitor the visitor 126 */ 127 void visit(SettingVisitor visitor); 128 129 /** 130 * Returns a setting whose value is null. 131 * 132 * Cannot be static, because there is no static inheritance. 133 * @return a Setting object that isn't null itself, but returns null 134 * for {@link #getValue()} 135 */ 136 Setting<T> getNullInstance(); 137 } 138 139 abstract public static class AbstractSetting<T> implements Setting<T> { 140 private T value; 141 public AbstractSetting(T value) { 142 this.value = value; 143 } 144 @Override 145 public T getValue() { 146 return value; 147 } 148 @Override 149 public String toString() { 150 return value != null ? value.toString() : "null"; 151 } 152 } 153 154 public static class StringSetting extends AbstractSetting<String> { 155 public StringSetting(String value) { 156 super(value); 157 } 158 public void visit(SettingVisitor visitor) { 159 visitor.visit(this); 160 } 161 public StringSetting getNullInstance() { 162 return new StringSetting(null); 163 } 164 } 165 166 public static class ListSetting extends AbstractSetting<List<String>> { 167 public ListSetting(List<String> value) { 168 super(value); 169 } 170 public void visit(SettingVisitor visitor) { 171 visitor.visit(this); 172 } 173 public ListSetting getNullInstance() { 174 return new ListSetting(null); 175 } 176 } 177 178 public static class ListListSetting extends AbstractSetting<List<List<String>>> { 179 public ListListSetting(List<List<String>> value) { 180 super(value); 181 } 182 public void visit(SettingVisitor visitor) { 183 visitor.visit(this); 184 } 185 public ListListSetting getNullInstance() { 186 return new ListListSetting(null); 187 } 188 } 189 190 public static class MapListSetting extends AbstractSetting<List<Map<String, String>>> { 191 public MapListSetting(List<Map<String, String>> value) { 192 super(value); 193 } 194 public void visit(SettingVisitor visitor) { 195 visitor.visit(this); 196 } 197 public MapListSetting getNullInstance() { 198 return new MapListSetting(null); 199 } 200 } 201 202 public interface SettingVisitor { 203 void visit(StringSetting setting); 204 void visit(ListSetting value); 205 void visit(ListListSetting value); 206 void visit(MapListSetting value); 207 } 208 209 public interface PreferenceChangeEvent<T> { 210 String getKey(); 211 Setting<T> getOldValue(); 212 Setting<T> getNewValue(); 213 } 214 215 public interface PreferenceChangedListener { 216 void preferenceChanged(PreferenceChangeEvent e); 217 } 218 219 private static class DefaultPreferenceChangeEvent<T> implements PreferenceChangeEvent<T> { 220 private final String key; 221 private final Setting<T> oldValue; 222 private final Setting<T> newValue; 223 224 public DefaultPreferenceChangeEvent(String key, Setting<T> oldValue, Setting<T> newValue) { 225 this.key = key; 226 this.oldValue = oldValue; 227 this.newValue = newValue; 228 } 229 230 public String getKey() { 231 return key; 232 } 233 public Setting<T> getOldValue() { 234 return oldValue; 235 } 236 public Setting<T> getNewValue() { 237 return newValue; 238 } 239 } 240 241 public interface ColorKey { 242 String getColorName(); 243 String getSpecialName(); 244 Color getDefaultValue(); 245 } 246 247 private final CopyOnWriteArrayList<PreferenceChangedListener> listeners = new CopyOnWriteArrayList<PreferenceChangedListener>(); 248 249 public void addPreferenceChangeListener(PreferenceChangedListener listener) { 250 if (listener != null) { 251 listeners.addIfAbsent(listener); 252 } 253 } 254 255 public void removePreferenceChangeListener(PreferenceChangedListener listener) { 256 listeners.remove(listener); 257 } 258 259 protected <T> void firePreferenceChanged(String key, Setting<T> oldValue, Setting<T> newValue) { 260 PreferenceChangeEvent<T> evt = new DefaultPreferenceChangeEvent<T>(key, oldValue, newValue); 261 for (PreferenceChangedListener l : listeners) { 262 l.preferenceChanged(evt); 263 } 264 } 265 266 /** 267 * Return the location of the user defined preferences file 268 */ 269 public String getPreferencesDir() { 270 final String path = getPreferencesDirFile().getPath(); 271 if (path.endsWith(File.separator)) 272 return path; 273 return path + File.separator; 274 } 275 276 public File getPreferencesDirFile() { 277 if (preferencesDirFile != null) 278 return preferencesDirFile; 279 String path; 280 path = System.getProperty("josm.home"); 281 if (path != null) { 282 preferencesDirFile = new File(path).getAbsoluteFile(); 283 } else { 284 path = System.getenv("APPDATA"); 285 if (path != null) { 286 preferencesDirFile = new File(path, "JOSM"); 287 } else { 288 preferencesDirFile = new File(System.getProperty("user.home"), ".josm"); 289 } 290 } 291 return preferencesDirFile; 292 } 293 294 public File getPreferenceFile() { 295 return new File(getPreferencesDirFile(), "preferences.xml"); 296 } 297 298 /* remove end of 2012 */ 299 public File getOldPreferenceFile() { 300 return new File(getPreferencesDirFile(), "preferences"); 301 } 302 303 public File getPluginsDirectory() { 304 return new File(getPreferencesDirFile(), "plugins"); 305 } 306 307 public File getCacheDirectory() { 308 if (cacheDirFile != null) 309 return cacheDirFile; 310 String path = System.getProperty("josm.cache"); 311 if (path != null) { 312 cacheDirFile = new File(path).getAbsoluteFile(); 313 } else { 314 path = Main.pref.get("cache.folder", null); 315 if (path != null) { 316 cacheDirFile = new File(path); 317 } else { 318 cacheDirFile = new File(getPreferencesDirFile(), "cache"); 319 } 320 } 321 if (!cacheDirFile.exists() && !cacheDirFile.mkdirs()) { 322 System.err.println(tr("Warning: Failed to create missing cache directory: {0}", cacheDirFile.getAbsoluteFile())); 323 JOptionPane.showMessageDialog( 324 Main.parent, 325 tr("<html>Failed to create missing cache directory: {0}</html>", cacheDirFile.getAbsoluteFile()), 326 tr("Error"), 327 JOptionPane.ERROR_MESSAGE 328 ); 329 } 330 return cacheDirFile; 331 } 332 333 /** 334 * @return A list of all existing directories where resources could be stored. 335 */ 336 public Collection<String> getAllPossiblePreferenceDirs() { 337 LinkedList<String> locations = new LinkedList<String>(); 338 locations.add(Main.pref.getPreferencesDir()); 339 String s; 340 if ((s = System.getenv("JOSM_RESOURCES")) != null) { 341 if (!s.endsWith(File.separator)) { 342 s = s + File.separator; 343 } 344 locations.add(s); 345 } 346 if ((s = System.getProperty("josm.resources")) != null) { 347 if (!s.endsWith(File.separator)) { 348 s = s + File.separator; 349 } 350 locations.add(s); 351 } 352 String appdata = System.getenv("APPDATA"); 353 if (System.getenv("ALLUSERSPROFILE") != null && appdata != null 354 && appdata.lastIndexOf(File.separator) != -1) { 355 appdata = appdata.substring(appdata.lastIndexOf(File.separator)); 356 locations.add(new File(new File(System.getenv("ALLUSERSPROFILE"), 357 appdata), "JOSM").getPath()); 358 } 359 locations.add("/usr/local/share/josm/"); 360 locations.add("/usr/local/lib/josm/"); 361 locations.add("/usr/share/josm/"); 362 locations.add("/usr/lib/josm/"); 363 return locations; 364 } 365 366 /** 367 * Get settings value for a certain key. 368 * @param key the identifier for the setting 369 * @return "" if there is nothing set for the preference key, 370 * the corresponding value otherwise. The result is not null. 371 */ 372 synchronized public String get(final String key) { 373 putDefault(key, null); 374 if (!properties.containsKey(key)) 375 return ""; 376 return properties.get(key); 377 } 378 379 /** 380 * Get settings value for a certain key and provide default a value. 381 * @param key the identifier for the setting 382 * @param def the default value. For each call of get() with a given key, the 383 * default value must be the same. 384 * @return the corresponding value if the property has been set before, 385 * def otherwise 386 */ 387 synchronized public String get(final String key, final String def) { 388 putDefault(key, def); 389 final String prop = properties.get(key); 390 if (prop == null || prop.equals("")) 391 return def; 392 return prop; 393 } 394 395 synchronized public Map<String, String> getAllPrefix(final String prefix) { 396 final Map<String,String> all = new TreeMap<String,String>(); 397 for (final Entry<String,String> e : properties.entrySet()) { 398 if (e.getKey().startsWith(prefix)) { 399 all.put(e.getKey(), e.getValue()); 400 } 401 } 402 return all; 403 } 404 405 synchronized public List<String> getAllPrefixCollectionKeys(final String prefix) { 406 final List<String> all = new LinkedList<String>(); 407 for (final String e : collectionProperties.keySet()) { 408 if (e.startsWith(prefix)) { 409 all.add(e); 410 } 411 } 412 return all; 413 } 414 415 synchronized private Map<String, String> getAllPrefixDefault(final String prefix) { 416 final Map<String,String> all = new TreeMap<String,String>(); 417 for (final Entry<String,String> e : defaults.entrySet()) { 418 if (e.getKey().startsWith(prefix)) { 419 all.put(e.getKey(), e.getValue()); 420 } 421 } 422 return all; 423 } 424 425 synchronized public TreeMap<String, String> getAllColors() { 426 final TreeMap<String,String> all = new TreeMap<String,String>(); 427 for (final Entry<String,String> e : defaults.entrySet()) { 428 if (e.getKey().startsWith("color.") && e.getValue() != null) { 429 all.put(e.getKey().substring(6), e.getValue()); 430 } 431 } 432 for (final Entry<String,String> e : properties.entrySet()) { 433 if (e.getKey().startsWith("color.")) { 434 all.put(e.getKey().substring(6), e.getValue()); 435 } 436 } 437 return all; 438 } 439 440 synchronized public Map<String, String> getDefaults() { 441 return defaults; 442 } 443 444 synchronized public void putDefault(final String key, final String def) { 445 if(!defaults.containsKey(key) || defaults.get(key) == null) { 446 defaults.put(key, def); 447 } else if(def != null && !defaults.get(key).equals(def)) { 448 System.out.println("Defaults for " + key + " differ: " + def + " != " + defaults.get(key)); 449 } 450 } 451 452 synchronized public boolean getBoolean(final String key) { 453 putDefault(key, null); 454 return properties.containsKey(key) ? Boolean.parseBoolean(properties.get(key)) : false; 455 } 456 457 synchronized public boolean getBoolean(final String key, final boolean def) { 458 putDefault(key, Boolean.toString(def)); 459 return properties.containsKey(key) ? Boolean.parseBoolean(properties.get(key)) : def; 460 } 461 462 synchronized public boolean getBoolean(final String key, final String specName, final boolean def) { 463 putDefault(key, Boolean.toString(def)); 464 String skey = key+"."+specName; 465 if(properties.containsKey(skey)) 466 return Boolean.parseBoolean(properties.get(skey)); 467 return properties.containsKey(key) ? Boolean.parseBoolean(properties.get(key)) : def; 468 } 469 470 /** 471 * Set a value for a certain setting. The changed setting is saved 472 * to the preference file immediately. Due to caching mechanisms on modern 473 * operating systems and hardware, this shouldn't be a performance problem. 474 * @param key the unique identifier for the setting 475 * @param value the value of the setting. Can be null or "" which both removes 476 * the key-value entry. 477 * @return if true, something has changed (i.e. value is different than before) 478 */ 479 public boolean put(final String key, String value) { 480 boolean changed = false; 481 String oldValue = null; 482 483 synchronized (this) { 484 oldValue = properties.get(key); 485 if(value != null && value.length() == 0) { 486 value = null; 487 } 488 // value is the same as before - no need to save anything 489 boolean equalValue = oldValue != null && oldValue.equals(value); 490 // The setting was previously unset and we are supposed to put a 491 // value that equals the default value. This is not necessary because 492 // the default value is the same throughout josm. In addition we like 493 // to have the possibility to change the default value from version 494 // to version, which would not work if we wrote it to the preference file. 495 boolean unsetIsDefault = oldValue == null && (value == null || value.equals(defaults.get(key))); 496 497 if (!(equalValue || unsetIsDefault)) { 498 if (value == null) { 499 properties.remove(key); 500 } else { 501 properties.put(key, value); 502 } 503 try { 504 save(); 505 } catch(IOException e){ 506 System.out.println(tr("Warning: failed to persist preferences to ''{0}''", getPreferenceFile().getAbsoluteFile())); 507 } 508 changed = true; 509 } 510 } 511 if (changed) { 512 // Call outside of synchronized section in case some listener wait for other thread that wait for preference lock 513 firePreferenceChanged(key, new StringSetting(oldValue), new StringSetting(value)); 514 } 515 return changed; 516 } 517 518 public boolean put(final String key, final boolean value) { 519 return put(key, Boolean.toString(value)); 520 } 521 522 public boolean putInteger(final String key, final Integer value) { 523 return put(key, Integer.toString(value)); 524 } 525 526 public boolean putDouble(final String key, final Double value) { 527 return put(key, Double.toString(value)); 528 } 529 530 public boolean putLong(final String key, final Long value) { 531 return put(key, Long.toString(value)); 532 } 533 534 /** 535 * Called after every put. In case of a problem, do nothing but output the error 536 * in log. 537 */ 538 public void save() throws IOException { 539 /* currently unused, but may help to fix configuration issues in future */ 540 putInteger("josm.version", Version.getInstance().getVersion()); 541 542 updateSystemProperties(); 543 if(Main.applet) 544 return; 545 546 File prefFile = getPreferenceFile(); 547 File backupFile = new File(prefFile + "_backup"); 548 549 // Backup old preferences if there are old preferences 550 if(prefFile.exists()) { 551 copyFile(prefFile, backupFile); 552 } 553 554 final PrintWriter out = new PrintWriter(new OutputStreamWriter( 555 new FileOutputStream(prefFile + "_tmp"), "utf-8"), false); 556 out.print(toXML(false)); 557 out.close(); 558 559 File tmpFile = new File(prefFile + "_tmp"); 560 copyFile(tmpFile, prefFile); 561 tmpFile.delete(); 562 563 setCorrectPermissions(prefFile); 564 setCorrectPermissions(backupFile); 565 } 566 567 568 private void setCorrectPermissions(File file) { 569 file.setReadable(false, false); 570 file.setWritable(false, false); 571 file.setExecutable(false, false); 572 file.setReadable(true, true); 573 file.setWritable(true, true); 574 } 575 576 /** 577 * Simple file copy function that will overwrite the target file 578 * Taken from http://www.rgagnon.com/javadetails/java-0064.html (CC-NC-BY-SA) 579 * @param in 580 * @param out 581 * @throws IOException 582 */ 583 public static void copyFile(File in, File out) throws IOException { 584 FileChannel inChannel = new FileInputStream(in).getChannel(); 585 FileChannel outChannel = new FileOutputStream(out).getChannel(); 586 try { 587 inChannel.transferTo(0, inChannel.size(), 588 outChannel); 589 } 590 catch (IOException e) { 591 throw e; 592 } 593 finally { 594 if (inChannel != null) { 595 inChannel.close(); 596 } 597 if (outChannel != null) { 598 outChannel.close(); 599 } 600 } 601 } 602 603 public void loadOld() throws Exception { 604 load(true); 605 } 606 607 public void load() throws Exception { 608 load(false); 609 } 610 611 private void load(boolean old) throws Exception { 612 properties.clear(); 613 if (!Main.applet) { 614 File pref = old ? getOldPreferenceFile() : getPreferenceFile(); 615 BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(pref), "utf-8")); 616 /* FIXME: TODO: remove old style config file end of 2012 */ 617 try { 618 if (old) { 619 in.mark(1); 620 int v = in.read(); 621 in.reset(); 622 if(v == '<') { 623 validateXML(in); 624 Utils.close(in); 625 in = new BufferedReader(new InputStreamReader(new FileInputStream(pref), "utf-8")); 626 fromXML(in); 627 } else { 628 int lineNumber = 0; 629 ArrayList<Integer> errLines = new ArrayList<Integer>(); 630 for (String line = in.readLine(); line != null; line = in.readLine(), lineNumber++) { 631 final int i = line.indexOf('='); 632 if (i == -1 || i == 0) { 633 errLines.add(lineNumber); 634 continue; 635 } 636 String key = line.substring(0,i); 637 String value = line.substring(i+1); 638 if (!value.isEmpty()) { 639 properties.put(key, value); 640 } 641 } 642 if (!errLines.isEmpty()) 643 throw new IOException(tr("Malformed config file at lines {0}", errLines)); 644 } 645 } else { 646 validateXML(in); 647 Utils.close(in); 648 in = new BufferedReader(new InputStreamReader(new FileInputStream(pref), "utf-8")); 649 fromXML(in); 650 } 651 } finally { 652 in.close(); 653 } 654 } 655 updateSystemProperties(); 656 /* FIXME: TODO: remove special version check end of 2012 */ 657 if(!properties.containsKey("expert")) { 658 try { 659 String v = get("josm.version"); 660 if(v.isEmpty() || Integer.parseInt(v) <= 4511) 661 properties.put("expert", "true"); 662 } catch(Exception e) { 663 properties.put("expert", "true"); 664 } 665 } 666 removeObsolete(); 667 } 668 669 public void init(boolean reset){ 670 if(Main.applet) 671 return; 672 // get the preferences. 673 File prefDir = getPreferencesDirFile(); 674 if (prefDir.exists()) { 675 if(!prefDir.isDirectory()) { 676 System.err.println(tr("Warning: Failed to initialize preferences. Preference directory ''{0}'' is not a directory.", prefDir.getAbsoluteFile())); 677 JOptionPane.showMessageDialog( 678 Main.parent, 679 tr("<html>Failed to initialize preferences.<br>Preference directory ''{0}'' is not a directory.</html>", prefDir.getAbsoluteFile()), 680 tr("Error"), 681 JOptionPane.ERROR_MESSAGE 682 ); 683 return; 684 } 685 } else { 686 if (! prefDir.mkdirs()) { 687 System.err.println(tr("Warning: Failed to initialize preferences. Failed to create missing preference directory: {0}", prefDir.getAbsoluteFile())); 688 JOptionPane.showMessageDialog( 689 Main.parent, 690 tr("<html>Failed to initialize preferences.<br>Failed to create missing preference directory: {0}</html>",prefDir.getAbsoluteFile()), 691 tr("Error"), 692 JOptionPane.ERROR_MESSAGE 693 ); 694 return; 695 } 696 } 697 698 File preferenceFile = getPreferenceFile(); 699 try { 700 if (!preferenceFile.exists()) { 701 File oldPreferenceFile = getOldPreferenceFile(); 702 if (!oldPreferenceFile.exists()) { 703 System.out.println(tr("Info: Missing preference file ''{0}''. Creating a default preference file.", preferenceFile.getAbsoluteFile())); 704 resetToDefault(); 705 save(); 706 } else { 707 try { 708 loadOld(); 709 } catch (Exception e) { 710 e.printStackTrace(); 711 File backupFile = new File(prefDir,"preferences.bak"); 712 JOptionPane.showMessageDialog( 713 Main.parent, 714 tr("<html>Preferences file had errors.<br> Making backup of old one to <br>{0}<br> and creating a new default preference file.</html>", backupFile.getAbsoluteFile()), 715 tr("Error"), 716 JOptionPane.ERROR_MESSAGE 717 ); 718 Main.platform.rename(oldPreferenceFile, backupFile); 719 try { 720 resetToDefault(); 721 save(); 722 } catch(IOException e1) { 723 e1.printStackTrace(); 724 System.err.println(tr("Warning: Failed to initialize preferences. Failed to reset preference file to default: {0}", getPreferenceFile())); 725 } 726 } 727 return; 728 } 729 } else if (reset) { 730 System.out.println(tr("Warning: Replacing existing preference file ''{0}'' with default preference file.", preferenceFile.getAbsoluteFile())); 731 resetToDefault(); 732 save(); 733 } 734 } catch(IOException e) { 735 e.printStackTrace(); 736 JOptionPane.showMessageDialog( 737 Main.parent, 738 tr("<html>Failed to initialize preferences.<br>Failed to reset preference file to default: {0}</html>",getPreferenceFile().getAbsoluteFile()), 739 tr("Error"), 740 JOptionPane.ERROR_MESSAGE 741 ); 742 return; 743 } 744 try { 745 load(); 746 } catch (Exception e) { 747 e.printStackTrace(); 748 File backupFile = new File(prefDir,"preferences.xml.bak"); 749 JOptionPane.showMessageDialog( 750 Main.parent, 751 tr("<html>Preferences file had errors.<br> Making backup of old one to <br>{0}<br> and creating a new default preference file.</html>", backupFile.getAbsoluteFile()), 752 tr("Error"), 753 JOptionPane.ERROR_MESSAGE 754 ); 755 Main.platform.rename(preferenceFile, backupFile); 756 try { 757 resetToDefault(); 758 save(); 759 } catch(IOException e1) { 760 e1.printStackTrace(); 761 System.err.println(tr("Warning: Failed to initialize preferences. Failed to reset preference file to default: {0}", getPreferenceFile())); 762 } 763 } 764 } 765 766 public final void resetToDefault(){ 767 properties.clear(); 768 } 769 770 /** 771 * Convenience method for accessing colour preferences. 772 * 773 * @param colName name of the colour 774 * @param def default value 775 * @return a Color object for the configured colour, or the default value if none configured. 776 */ 777 synchronized public Color getColor(String colName, Color def) { 778 return getColor(colName, null, def); 779 } 780 781 synchronized public Color getUIColor(String colName) { 782 return UIManager.getColor(colName); 783 } 784 785 /* only for preferences */ 786 synchronized public String getColorName(String o) { 787 try 788 { 789 Matcher m = Pattern.compile("mappaint\\.(.+?)\\.(.+)").matcher(o); 790 m.matches(); 791 return tr("Paint style {0}: {1}", tr(m.group(1)), tr(m.group(2))); 792 } 793 catch (Exception e) {} 794 try 795 { 796 Matcher m = Pattern.compile("layer (.+)").matcher(o); 797 m.matches(); 798 return tr("Layer: {0}", tr(m.group(1))); 799 } 800 catch (Exception e) {} 801 return tr(colornames.containsKey(o) ? colornames.get(o) : o); 802 } 803 804 public Color getColor(ColorKey key) { 805 return getColor(key.getColorName(), key.getSpecialName(), key.getDefaultValue()); 806 } 807 808 /** 809 * Convenience method for accessing colour preferences. 810 * 811 * @param colName name of the colour 812 * @param specName name of the special colour settings 813 * @param def default value 814 * @return a Color object for the configured colour, or the default value if none configured. 815 */ 816 synchronized public Color getColor(String colName, String specName, Color def) { 817 String colKey = ColorProperty.getColorKey(colName); 818 if(!colKey.equals(colName)) { 819 colornames.put(colKey, colName); 820 } 821 putDefault("color."+colKey, ColorHelper.color2html(def)); 822 String colStr = specName != null ? get("color."+specName) : ""; 823 if(colStr.equals("")) { 824 colStr = get("color."+colKey); 825 } 826 return colStr.equals("") ? def : ColorHelper.html2color(colStr); 827 } 828 829 synchronized public Color getDefaultColor(String colKey) { 830 String colStr = defaults.get("color."+colKey); 831 return colStr == null || "".equals(colStr) ? null : ColorHelper.html2color(colStr); 832 } 833 834 synchronized public boolean putColor(String colKey, Color val) { 835 return put("color."+colKey, val != null ? ColorHelper.color2html(val) : null); 836 } 837 838 synchronized public int getInteger(String key, int def) { 839 putDefault(key, Integer.toString(def)); 840 String v = get(key); 841 if(v.isEmpty()) 842 return def; 843 844 try { 845 return Integer.parseInt(v); 846 } catch(NumberFormatException e) { 847 // fall out 848 } 849 return def; 850 } 851 852 synchronized public int getInteger(String key, String specName, int def) { 853 putDefault(key, Integer.toString(def)); 854 String v = get(key+"."+specName); 855 if(v.isEmpty()) 856 v = get(key); 857 if(v.isEmpty()) 858 return def; 859 860 try { 861 return Integer.parseInt(v); 862 } catch(NumberFormatException e) { 863 // fall out 864 } 865 return def; 866 } 867 868 synchronized public long getLong(String key, long def) { 869 putDefault(key, Long.toString(def)); 870 String v = get(key); 871 if(null == v) 872 return def; 873 874 try { 875 return Long.parseLong(v); 876 } catch(NumberFormatException e) { 877 // fall out 878 } 879 return def; 880 } 881 882 synchronized public double getDouble(String key, double def) { 883 putDefault(key, Double.toString(def)); 884 String v = get(key); 885 if(null == v) 886 return def; 887 888 try { 889 return Double.parseDouble(v); 890 } catch(NumberFormatException e) { 891 // fall out 892 } 893 return def; 894 } 895 896 synchronized public double getDouble(String key, String def) { 897 putDefault(key, def); 898 String v = get(key); 899 if(v != null && v.length() != 0) { 900 try { return Double.parseDouble(v); } catch(NumberFormatException e) {} 901 } 902 try { return Double.parseDouble(def); } catch(NumberFormatException e) {} 903 return 0.0; 904 } 905 906 /** 907 * Get a list of values for a certain key 908 * @param key the identifier for the setting 909 * @param def the default value. 910 * @return the corresponding value if the property has been set before, 911 * def otherwise 912 */ 913 public Collection<String> getCollection(String key, Collection<String> def) { 914 putCollectionDefault(key, def == null ? null : new ArrayList<String>(def)); 915 Collection<String> prop = getCollectionInternal(key); 916 if (prop != null) 917 return prop; 918 else 919 return def; 920 } 921 922 /** 923 * Get a list of values for a certain key 924 * @param key the identifier for the setting 925 * @return the corresponding value if the property has been set before, 926 * an empty Collection otherwise. 927 */ 928 public Collection<String> getCollection(String key) { 929 putCollectionDefault(key, null); 930 Collection<String> prop = getCollectionInternal(key); 931 if (prop != null) 932 return prop; 933 else 934 return Collections.emptyList(); 935 } 936 937 /* remove this workaround end of 2012, replace by direct access to structure */ 938 synchronized private List<String> getCollectionInternal(String key) { 939 List<String> prop = collectionProperties.get(key); 940 if (prop != null) 941 return prop; 942 else { 943 String s = properties.get(key); 944 if(s != null) { 945 prop = Arrays.asList(s.split("\u001e", -1)); 946 collectionProperties.put(key, Collections.unmodifiableList(prop)); 947 properties.remove(key); 948 defaults.remove(key); 949 return prop; 950 } 951 } 952 return null; 953 } 954 955 synchronized public void removeFromCollection(String key, String value) { 956 List<String> a = new ArrayList<String>(getCollection(key, Collections.<String>emptyList())); 957 a.remove(value); 958 putCollection(key, a); 959 } 960 961 public boolean putCollection(String key, Collection<String> value) { 962 List<String> oldValue = null; 963 List<String> valueCopy = null; 964 965 synchronized (this) { 966 if (value == null) { 967 oldValue = collectionProperties.remove(key); 968 boolean changed = oldValue != null; 969 changed |= properties.remove(key) != null; 970 if (!changed) return false; 971 } else { 972 oldValue = getCollectionInternal(key); 973 if (equalCollection(value, oldValue)) return false; 974 Collection<String> defValue = collectionDefaults.get(key); 975 if (oldValue == null && equalCollection(value, defValue)) return false; 976 977 valueCopy = new ArrayList<String>(value); 978 if (valueCopy.contains(null)) throw new RuntimeException("Error: Null as list element in preference setting (key '"+key+"')"); 979 collectionProperties.put(key, Collections.unmodifiableList(valueCopy)); 980 } 981 try { 982 save(); 983 } catch(IOException e){ 984 System.out.println(tr("Warning: failed to persist preferences to ''{0}''", getPreferenceFile().getAbsoluteFile())); 985 } 986 } 987 // Call outside of synchronized section in case some listener wait for other thread that wait for preference lock 988 firePreferenceChanged(key, new ListSetting(oldValue), new ListSetting(valueCopy)); 989 return true; 990 } 991 992 public static boolean equalCollection(Collection<String> a, Collection<String> b) { 993 if (a == null) return b == null; 994 if (b == null) return false; 995 if (a.size() != b.size()) return false; 996 Iterator<String> itA = a.iterator(); 997 Iterator<String> itB = b.iterator(); 998 while (itA.hasNext()) { 999 String aStr = itA.next(); 1000 String bStr = itB.next(); 1001 if (!Utils.equal(aStr,bStr)) return false; 1002 } 1003 return true; 1004 } 1005 1006 /** 1007 * Saves at most {@code maxsize} items of collection {@code val}. 1008 */ 1009 public boolean putCollectionBounded(String key, int maxsize, Collection<String> val) { 1010 Collection<String> newCollection = new ArrayList<String>(Math.min(maxsize, val.size())); 1011 for (String i : val) { 1012 if (newCollection.size() >= maxsize) { 1013 break; 1014 } 1015 newCollection.add(i); 1016 } 1017 return putCollection(key, newCollection); 1018 } 1019 1020 synchronized private void putCollectionDefault(String key, List<String> val) { 1021 collectionDefaults.put(key, val); 1022 } 1023 1024 /** 1025 * Used to read a 2-dimensional array of strings from the preference file. 1026 * If not a single entry could be found, def is returned. 1027 */ 1028 synchronized public Collection<Collection<String>> getArray(String key, Collection<Collection<String>> def) { 1029 if (def != null) { 1030 List<List<String>> defCopy = new ArrayList<List<String>>(def.size()); 1031 for (Collection<String> lst : def) { 1032 defCopy.add(Collections.unmodifiableList(new ArrayList<String>(lst))); 1033 } 1034 putArrayDefault(key, Collections.unmodifiableList(defCopy)); 1035 } else { 1036 putArrayDefault(key, null); 1037 } 1038 List<List<String>> prop = getArrayInternal(key); 1039 if (prop != null) { 1040 @SuppressWarnings("unchecked") 1041 Collection<Collection<String>> prop_cast = (Collection) prop; 1042 return prop_cast; 1043 } else 1044 return def; 1045 } 1046 1047 public Collection<Collection<String>> getArray(String key) { 1048 putArrayDefault(key, null); 1049 List<List<String>> prop = getArrayInternal(key); 1050 if (prop != null) { 1051 @SuppressWarnings("unchecked") 1052 Collection<Collection<String>> prop_cast = (Collection) prop; 1053 return prop_cast; 1054 } else 1055 return Collections.emptyList(); 1056 } 1057 1058 /* remove this workaround end of 2012 and replace by direct array access */ 1059 synchronized private List<List<String>> getArrayInternal(String key) { 1060 List<List<String>> prop = arrayProperties.get(key); 1061 if (prop != null) 1062 return prop; 1063 else { 1064 String keyDot = key + "."; 1065 int num = 0; 1066 List<List<String>> col = new ArrayList<List<String>>(); 1067 while (true) { 1068 List<String> c = getCollectionInternal(keyDot+num); 1069 if (c == null) { 1070 break; 1071 } 1072 col.add(c); 1073 collectionProperties.remove(keyDot+num); 1074 collectionDefaults.remove(keyDot+num); 1075 num++; 1076 } 1077 if (num > 0) { 1078 arrayProperties.put(key, Collections.unmodifiableList(col)); 1079 return col; 1080 } 1081 } 1082 return null; 1083 } 1084 1085 public boolean putArray(String key, Collection<Collection<String>> value) { 1086 boolean changed = false; 1087 1088 List<List<String>> oldValue = null; 1089 List<List<String>> valueCopy = null; 1090 1091 synchronized (this) { 1092 if (value == null) { 1093 oldValue = getArrayInternal(key); 1094 if (arrayProperties.remove(key) != null) return false; 1095 } else { 1096 oldValue = getArrayInternal(key); 1097 if (equalArray(value, oldValue)) return false; 1098 1099 List<List<String>> defValue = arrayDefaults.get(key); 1100 if (oldValue == null && equalArray(value, defValue)) return false; 1101 1102 valueCopy = new ArrayList<List<String>>(value.size()); 1103 if (valueCopy.contains(null)) throw new RuntimeException("Error: Null as list element in preference setting (key '"+key+"')"); 1104 for (Collection<String> lst : value) { 1105 List<String> lstCopy = new ArrayList<String>(lst); 1106 if (lstCopy.contains(null)) throw new RuntimeException("Error: Null as inner list element in preference setting (key '"+key+"')"); 1107 valueCopy.add(Collections.unmodifiableList(lstCopy)); 1108 } 1109 arrayProperties.put(key, Collections.unmodifiableList(valueCopy)); 1110 } 1111 try { 1112 save(); 1113 } catch(IOException e){ 1114 System.out.println(tr("Warning: failed to persist preferences to ''{0}''", getPreferenceFile().getAbsoluteFile())); 1115 } 1116 } 1117 // Call outside of synchronized section in case some listener wait for other thread that wait for preference lock 1118 firePreferenceChanged(key, new ListListSetting(oldValue), new ListListSetting(valueCopy)); 1119 return true; 1120 } 1121 1122 public static boolean equalArray(Collection<Collection<String>> a, Collection<List<String>> b) { 1123 if (a == null) return b == null; 1124 if (b == null) return false; 1125 if (a.size() != b.size()) return false; 1126 Iterator<Collection<String>> itA = a.iterator(); 1127 Iterator<List<String>> itB = b.iterator(); 1128 while (itA.hasNext()) { 1129 if (!equalCollection(itA.next(), itB.next())) return false; 1130 } 1131 return true; 1132 } 1133 1134 synchronized private void putArrayDefault(String key, List<List<String>> val) { 1135 arrayDefaults.put(key, val); 1136 } 1137 1138 public Collection<Map<String, String>> getListOfStructs(String key, Collection<Map<String, String>> def) { 1139 if (def != null) { 1140 List<Map<String, String>> defCopy = new ArrayList<Map<String, String>>(def.size()); 1141 for (Map<String, String> map : def) { 1142 defCopy.add(Collections.unmodifiableMap(new LinkedHashMap<String,String>(map))); 1143 } 1144 putListOfStructsDefault(key, Collections.unmodifiableList(defCopy)); 1145 } else { 1146 putListOfStructsDefault(key, null); 1147 } 1148 Collection<Map<String, String>> prop = getListOfStructsInternal(key); 1149 if (prop != null) 1150 return prop; 1151 else 1152 return def; 1153 } 1154 1155 /* remove this workaround end of 2012 and use direct access to proper variable */ 1156 private synchronized List<Map<String, String>> getListOfStructsInternal(String key) { 1157 List<Map<String, String>> prop = listOfStructsProperties.get(key); 1158 if (prop != null) 1159 return prop; 1160 else { 1161 List<List<String>> array = getArrayInternal(key); 1162 if (array == null) return null; 1163 prop = new ArrayList<Map<String, String>>(array.size()); 1164 for (Collection<String> mapStr : array) { 1165 Map<String, String> map = new LinkedHashMap<String, String>(); 1166 for (String key_value : mapStr) { 1167 final int i = key_value.indexOf(':'); 1168 if (i == -1 || i == 0) { 1169 continue; 1170 } 1171 String k = key_value.substring(0,i); 1172 String v = key_value.substring(i+1); 1173 map.put(k, v); 1174 } 1175 prop.add(Collections.unmodifiableMap(map)); 1176 } 1177 arrayProperties.remove(key); 1178 arrayDefaults.remove(key); 1179 listOfStructsProperties.put(key, Collections.unmodifiableList(prop)); 1180 return prop; 1181 } 1182 } 1183 1184 public boolean putListOfStructs(String key, Collection<Map<String, String>> value) { 1185 boolean changed = false; 1186 1187 List<Map<String, String>> oldValue; 1188 List<Map<String, String>> valueCopy = null; 1189 1190 synchronized (this) { 1191 if (value == null) { 1192 oldValue = getListOfStructsInternal(key); 1193 if (listOfStructsProperties.remove(key) != null) return false; 1194 } else { 1195 oldValue = getListOfStructsInternal(key); 1196 if (equalListOfStructs(oldValue, value)) return false; 1197 1198 List<Map<String, String>> defValue = listOfStructsDefaults.get(key); 1199 if (oldValue == null && equalListOfStructs(value, defValue)) return false; 1200 1201 valueCopy = new ArrayList<Map<String, String>>(value.size()); 1202 if (valueCopy.contains(null)) throw new RuntimeException("Error: Null as list element in preference setting (key '"+key+"')"); 1203 for (Map<String, String> map : value) { 1204 Map<String, String> mapCopy = new LinkedHashMap<String,String>(map); 1205 if (mapCopy.keySet().contains(null)) throw new RuntimeException("Error: Null as map key in preference setting (key '"+key+"')"); 1206 if (mapCopy.values().contains(null)) throw new RuntimeException("Error: Null as map value in preference setting (key '"+key+"')"); 1207 valueCopy.add(Collections.unmodifiableMap(mapCopy)); 1208 } 1209 listOfStructsProperties.put(key, Collections.unmodifiableList(valueCopy)); 1210 } 1211 try { 1212 save(); 1213 } catch(IOException e){ 1214 System.out.println(tr("Warning: failed to persist preferences to ''{0}''", getPreferenceFile().getAbsoluteFile())); 1215 } 1216 } 1217 // Call outside of synchronized section in case some listener wait for other thread that wait for preference lock 1218 firePreferenceChanged(key, new MapListSetting(oldValue), new MapListSetting(valueCopy)); 1219 return true; 1220 } 1221 1222 public static boolean equalListOfStructs(Collection<Map<String, String>> a, Collection<Map<String, String>> b) { 1223 if (a == null) return b == null; 1224 if (b == null) return false; 1225 if (a.size() != b.size()) return false; 1226 Iterator<Map<String, String>> itA = a.iterator(); 1227 Iterator<Map<String, String>> itB = b.iterator(); 1228 while (itA.hasNext()) { 1229 if (!equalMap(itA.next(), itB.next())) return false; 1230 } 1231 return true; 1232 } 1233 1234 private static boolean equalMap(Map<String, String> a, Map<String, String> b) { 1235 if (a == null) return b == null; 1236 if (b == null) return false; 1237 if (a.size() != b.size()) return false; 1238 for (Entry<String, String> e : a.entrySet()) { 1239 if (!Utils.equal(e.getValue(), b.get(e.getKey()))) return false; 1240 } 1241 return true; 1242 } 1243 1244 synchronized private void putListOfStructsDefault(String key, List<Map<String, String>> val) { 1245 listOfStructsDefaults.put(key, val); 1246 } 1247 1248 @Retention(RetentionPolicy.RUNTIME) public @interface pref { } 1249 @Retention(RetentionPolicy.RUNTIME) public @interface writeExplicitly { } 1250 1251 /** 1252 * Get a list of hashes which are represented by a struct-like class. 1253 * Possible properties are given by fields of the class klass that have 1254 * the @pref annotation. 1255 * Default constructor is used to initialize the struct objects, properties 1256 * then override some of these default values. 1257 * @param key main preference key 1258 * @param klass The struct class 1259 * @return a list of objects of type T or an empty list if nothing was found 1260 */ 1261 public <T> List<T> getListOfStructs(String key, Class<T> klass) { 1262 List<T> r = getListOfStructs(key, null, klass); 1263 if (r == null) 1264 return Collections.emptyList(); 1265 else 1266 return r; 1267 } 1268 1269 /** 1270 * same as above, but returns def if nothing was found 1271 */ 1272 public <T> List<T> getListOfStructs(String key, Collection<T> def, Class<T> klass) { 1273 Collection<Map<String,String>> prop = 1274 getListOfStructs(key, def == null ? null : serializeListOfStructs(def, klass)); 1275 if (prop == null) 1276 return def == null ? null : new ArrayList<T>(def); 1277 List<T> lst = new ArrayList<T>(); 1278 for (Map<String,String> entries : prop) { 1279 T struct = deserializeStruct(entries, klass); 1280 lst.add(struct); 1281 } 1282 return lst; 1283 } 1284 1285 /** 1286 * Save a list of hashes represented by a struct-like class. 1287 * Considers only fields that have the @pref annotation. 1288 * In addition it does not write fields with null values. (Thus they are cleared) 1289 * Default values are given by the field values after default constructor has 1290 * been called. 1291 * Fields equal to the default value are not written unless the field has 1292 * the @writeExplicitly annotation. 1293 * @param key main preference key 1294 * @param val the list that is supposed to be saved 1295 * @param klass The struct class 1296 * @return true if something has changed 1297 */ 1298 public <T> boolean putListOfStructs(String key, Collection<T> val, Class<T> klass) { 1299 return putListOfStructs(key, serializeListOfStructs(val, klass)); 1300 } 1301 1302 private <T> Collection<Map<String,String>> serializeListOfStructs(Collection<T> l, Class<T> klass) { 1303 if (l == null) 1304 return null; 1305 Collection<Map<String,String>> vals = new ArrayList<Map<String,String>>(); 1306 for (T struct : l) { 1307 if (struct == null) { 1308 continue; 1309 } 1310 vals.add(serializeStruct(struct, klass)); 1311 } 1312 return vals; 1313 } 1314 1315 public static <T> Map<String,String> serializeStruct(T struct, Class<T> klass) { 1316 T structPrototype; 1317 try { 1318 structPrototype = klass.newInstance(); 1319 } catch (InstantiationException ex) { 1320 throw new RuntimeException(ex); 1321 } catch (IllegalAccessException ex) { 1322 throw new RuntimeException(ex); 1323 } 1324 1325 Map<String,String> hash = new LinkedHashMap<String,String>(); 1326 for (Field f : klass.getDeclaredFields()) { 1327 if (f.getAnnotation(pref.class) == null) { 1328 continue; 1329 } 1330 f.setAccessible(true); 1331 try { 1332 Object fieldValue = f.get(struct); 1333 Object defaultFieldValue = f.get(structPrototype); 1334 if (fieldValue != null) { 1335 if (f.getAnnotation(writeExplicitly.class) != null || !Utils.equal(fieldValue, defaultFieldValue)) { 1336 hash.put(f.getName().replace("_", "-"), fieldValue.toString()); 1337 } 1338 } 1339 } catch (IllegalArgumentException ex) { 1340 throw new RuntimeException(); 1341 } catch (IllegalAccessException ex) { 1342 throw new RuntimeException(); 1343 } 1344 } 1345 return hash; 1346 } 1347 1348 public static <T> T deserializeStruct(Map<String,String> hash, Class<T> klass) { 1349 T struct = null; 1350 try { 1351 struct = klass.newInstance(); 1352 } catch (InstantiationException ex) { 1353 throw new RuntimeException(); 1354 } catch (IllegalAccessException ex) { 1355 throw new RuntimeException(); 1356 } 1357 for (Entry<String,String> key_value : hash.entrySet()) { 1358 Object value = null; 1359 Field f; 1360 try { 1361 f = klass.getDeclaredField(key_value.getKey().replace("-", "_")); 1362 } catch (NoSuchFieldException ex) { 1363 continue; 1364 } catch (SecurityException ex) { 1365 throw new RuntimeException(); 1366 } 1367 if (f.getAnnotation(pref.class) == null) { 1368 continue; 1369 } 1370 f.setAccessible(true); 1371 if (f.getType() == Boolean.class || f.getType() == boolean.class) { 1372 value = Boolean.parseBoolean(key_value.getValue()); 1373 } else if (f.getType() == Integer.class || f.getType() == int.class) { 1374 try { 1375 value = Integer.parseInt(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.parseDouble(key_value.getValue()); 1382 } catch (NumberFormatException nfe) { 1383 continue; 1384 } 1385 } else if (f.getType() == String.class) { 1386 value = 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(); 1394 } catch (IllegalAccessException ex) { 1395 throw new RuntimeException(); 1396 } 1397 } 1398 return struct; 1399 } 1400 1401 public boolean putSetting(final String key, Setting value) { 1402 if (value == null) return false; 1403 class PutVisitor implements SettingVisitor { 1404 public boolean changed; 1405 public void visit(StringSetting setting) { 1406 changed = put(key, setting.getValue()); 1407 } 1408 public void visit(ListSetting setting) { 1409 changed = putCollection(key, setting.getValue()); 1410 } 1411 public void visit(ListListSetting setting) { 1412 @SuppressWarnings("unchecked") 1413 boolean changed = putArray(key, (Collection) setting.getValue()); 1414 this.changed = changed; 1415 } 1416 public void visit(MapListSetting setting) { 1417 changed = putListOfStructs(key, setting.getValue()); 1418 } 1419 }; 1420 PutVisitor putVisitor = new PutVisitor(); 1421 value.visit(putVisitor); 1422 return putVisitor.changed; 1423 } 1424 1425 public Map<String, Setting> getAllSettings() { 1426 Map<String, Setting> settings = new TreeMap<String, Setting>(); 1427 1428 for (Entry<String, String> e : properties.entrySet()) { 1429 settings.put(e.getKey(), new StringSetting(e.getValue())); 1430 } 1431 for (Entry<String, List<String>> e : collectionProperties.entrySet()) { 1432 settings.put(e.getKey(), new ListSetting(e.getValue())); 1433 } 1434 for (Entry<String, List<List<String>>> e : arrayProperties.entrySet()) { 1435 settings.put(e.getKey(), new ListListSetting(e.getValue())); 1436 } 1437 for (Entry<String, List<Map<String, String>>> e : listOfStructsProperties.entrySet()) { 1438 settings.put(e.getKey(), new MapListSetting(e.getValue())); 1439 } 1440 return settings; 1441 } 1442 1443 public Map<String, Setting> getAllDefaults() { 1444 Map<String, Setting> allDefaults = new TreeMap<String, Setting>(); 1445 1446 for (Entry<String, String> e : defaults.entrySet()) { 1447 allDefaults.put(e.getKey(), new StringSetting(e.getValue())); 1448 } 1449 for (Entry<String, List<String>> e : collectionDefaults.entrySet()) { 1450 allDefaults.put(e.getKey(), new ListSetting(e.getValue())); 1451 } 1452 for (Entry<String, List<List<String>>> e : arrayDefaults.entrySet()) { 1453 allDefaults.put(e.getKey(), new ListListSetting(e.getValue())); 1454 } 1455 for (Entry<String, List<Map<String, String>>> e : listOfStructsDefaults.entrySet()) { 1456 allDefaults.put(e.getKey(), new MapListSetting(e.getValue())); 1457 } 1458 return allDefaults; 1459 } 1460 1461 /** 1462 * Updates system properties with the current values in the preferences. 1463 * 1464 */ 1465 public void updateSystemProperties() { 1466 updateSystemProperty("http.agent", Version.getInstance().getAgentString()); 1467 updateSystemProperty("user.language", Main.pref.get("language")); 1468 // Workaround to fix a Java bug. 1469 // Force AWT toolkit to update its internal preferences (fix #3645). 1470 // This ugly hack comes from Sun bug database: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6292739 1471 try { 1472 Field field = Toolkit.class.getDeclaredField("resources"); 1473 field.setAccessible(true); 1474 field.set(null, ResourceBundle.getBundle("sun.awt.resources.awt")); 1475 } catch (Exception e) { 1476 // Ignore all exceptions 1477 } 1478 } 1479 1480 private void updateSystemProperty(String key, String value) { 1481 if (value != null) { 1482 System.setProperty(key, value); 1483 } 1484 } 1485 1486 /** 1487 * The default plugin site 1488 */ 1489 private final static String[] DEFAULT_PLUGIN_SITE = { 1490 "http://josm.openstreetmap.de/plugin%<?plugins=>"}; 1491 1492 /** 1493 * Replies the collection of plugin site URLs from where plugin lists can be downloaded 1494 * 1495 * @return 1496 */ 1497 public Collection<String> getPluginSites() { 1498 return getCollection("pluginmanager.sites", Arrays.asList(DEFAULT_PLUGIN_SITE)); 1499 } 1500 1501 /** 1502 * Sets the collection of plugin site URLs. 1503 * 1504 * @param sites the site URLs 1505 */ 1506 public void setPluginSites(Collection<String> sites) { 1507 putCollection("pluginmanager.sites", sites); 1508 } 1509 1510 protected XMLStreamReader parser; 1511 1512 public void validateXML(Reader in) throws Exception { 1513 SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); 1514 Schema schema = factory.newSchema(new StreamSource(new MirroredInputStream("resource://data/preferences.xsd"))); 1515 Validator validator = schema.newValidator(); 1516 validator.validate(new StreamSource(in)); 1517 } 1518 1519 public void fromXML(Reader in) throws XMLStreamException { 1520 XMLStreamReader parser = XMLInputFactory.newInstance().createXMLStreamReader(in); 1521 this.parser = parser; 1522 parse(); 1523 } 1524 1525 public void parse() throws XMLStreamException { 1526 int event = parser.getEventType(); 1527 while (true) { 1528 if (event == XMLStreamConstants.START_ELEMENT) { 1529 parseRoot(); 1530 } else if (event == XMLStreamConstants.END_ELEMENT) { 1531 return; 1532 } 1533 if (parser.hasNext()) { 1534 event = parser.next(); 1535 } else { 1536 break; 1537 } 1538 } 1539 parser.close(); 1540 } 1541 1542 public void parseRoot() throws XMLStreamException { 1543 while (true) { 1544 int event = parser.next(); 1545 if (event == XMLStreamConstants.START_ELEMENT) { 1546 if (parser.getLocalName().equals("tag")) { 1547 properties.put(parser.getAttributeValue(null, "key"), parser.getAttributeValue(null, "value")); 1548 jumpToEnd(); 1549 } else if (parser.getLocalName().equals("list") || 1550 parser.getLocalName().equals("collection") || 1551 parser.getLocalName().equals("lists") || 1552 parser.getLocalName().equals("maps") 1553 ) { 1554 parseToplevelList(); 1555 } else { 1556 throwException("Unexpected element: "+parser.getLocalName()); 1557 } 1558 } else if (event == XMLStreamConstants.END_ELEMENT) { 1559 return; 1560 } 1561 } 1562 } 1563 1564 private void jumpToEnd() throws XMLStreamException { 1565 while (true) { 1566 int event = parser.next(); 1567 if (event == XMLStreamConstants.START_ELEMENT) { 1568 jumpToEnd(); 1569 } else if (event == XMLStreamConstants.END_ELEMENT) { 1570 return; 1571 } 1572 } 1573 } 1574 1575 protected void parseToplevelList() throws XMLStreamException { 1576 String key = parser.getAttributeValue(null, "key"); 1577 String name = parser.getLocalName(); 1578 1579 List<String> entries = null; 1580 List<List<String>> lists = null; 1581 List<Map<String, String>> maps = null; 1582 while (true) { 1583 int event = parser.next(); 1584 if (event == XMLStreamConstants.START_ELEMENT) { 1585 if (parser.getLocalName().equals("entry")) { 1586 if (entries == null) { 1587 entries = new ArrayList<String>(); 1588 } 1589 entries.add(parser.getAttributeValue(null, "value")); 1590 jumpToEnd(); 1591 } else if (parser.getLocalName().equals("list")) { 1592 if (lists == null) { 1593 lists = new ArrayList<List<String>>(); 1594 } 1595 lists.add(parseInnerList()); 1596 } else if (parser.getLocalName().equals("map")) { 1597 if (maps == null) { 1598 maps = new ArrayList<Map<String, String>>(); 1599 } 1600 maps.add(parseMap()); 1601 } else { 1602 throwException("Unexpected element: "+parser.getLocalName()); 1603 } 1604 } else if (event == XMLStreamConstants.END_ELEMENT) { 1605 break; 1606 } 1607 } 1608 if (entries != null) { 1609 collectionProperties.put(key, Collections.unmodifiableList(entries)); 1610 } else if (lists != null) { 1611 arrayProperties.put(key, Collections.unmodifiableList(lists)); 1612 } else if (maps != null) { 1613 listOfStructsProperties.put(key, Collections.unmodifiableList(maps)); 1614 } else { 1615 if (name.equals("lists")) { 1616 arrayProperties.put(key, Collections.<List<String>>emptyList()); 1617 } else if (name.equals("maps")) { 1618 listOfStructsProperties.put(key, Collections.<Map<String, String>>emptyList()); 1619 } else { 1620 collectionProperties.put(key, Collections.<String>emptyList()); 1621 } 1622 } 1623 } 1624 1625 protected List<String> parseInnerList() throws XMLStreamException { 1626 List<String> entries = new ArrayList<String>(); 1627 while (true) { 1628 int event = parser.next(); 1629 if (event == XMLStreamConstants.START_ELEMENT) { 1630 if (parser.getLocalName().equals("entry")) { 1631 entries.add(parser.getAttributeValue(null, "value")); 1632 jumpToEnd(); 1633 } else { 1634 throwException("Unexpected element: "+parser.getLocalName()); 1635 } 1636 } else if (event == XMLStreamConstants.END_ELEMENT) { 1637 break; 1638 } 1639 } 1640 return Collections.unmodifiableList(entries); 1641 } 1642 1643 protected Map<String, String> parseMap() throws XMLStreamException { 1644 Map<String, String> map = new LinkedHashMap<String, String>(); 1645 while (true) { 1646 int event = parser.next(); 1647 if (event == XMLStreamConstants.START_ELEMENT) { 1648 if (parser.getLocalName().equals("tag")) { 1649 map.put(parser.getAttributeValue(null, "key"), parser.getAttributeValue(null, "value")); 1650 jumpToEnd(); 1651 } else { 1652 throwException("Unexpected element: "+parser.getLocalName()); 1653 } 1654 } else if (event == XMLStreamConstants.END_ELEMENT) { 1655 break; 1656 } 1657 } 1658 return Collections.unmodifiableMap(map); 1659 } 1660 1661 protected void throwException(String msg) { 1662 throw new RuntimeException(msg + tr(" (at line {0}, column {1})", parser.getLocation().getLineNumber(), parser.getLocation().getColumnNumber())); 1663 } 1664 1665 private class SettingToXml implements SettingVisitor { 1666 private StringBuilder b; 1667 private boolean noPassword; 1668 private String key; 1669 1670 public SettingToXml(StringBuilder b, boolean noPassword) { 1671 this.b = b; 1672 this.noPassword = noPassword; 1673 } 1674 1675 public void setKey(String key) { 1676 this.key = key; 1677 } 1678 1679 public void visit(StringSetting setting) { 1680 if (noPassword && key.equals("osm-server.password")) 1681 return; // do not store plain password. 1682 String r = setting.getValue(); 1683 String s = defaults.get(key); 1684 /* don't save default values */ 1685 if(s == null || !s.equals(r)) { 1686 /* TODO: remove old format exception end of 2012 */ 1687 if(r.contains("\u001e")) 1688 { 1689 b.append(" <list key='"); 1690 b.append(XmlWriter.encode(key)); 1691 b.append("'>\n"); 1692 for (String val : r.split("\u001e", -1)) 1693 { 1694 b.append(" <entry value='"); 1695 b.append(XmlWriter.encode(val)); 1696 b.append("'/>\n"); 1697 } 1698 b.append(" </list>\n"); 1699 } 1700 else 1701 { 1702 b.append(" <tag key='"); 1703 b.append(XmlWriter.encode(key)); 1704 b.append("' value='"); 1705 b.append(XmlWriter.encode(setting.getValue())); 1706 b.append("'/>\n"); 1707 } 1708 } 1709 } 1710 1711 public void visit(ListSetting setting) { 1712 b.append(" <list key='").append(XmlWriter.encode(key)).append("'>\n"); 1713 for (String s : setting.getValue()) { 1714 b.append(" <entry value='").append(XmlWriter.encode(s)).append("'/>\n"); 1715 } 1716 b.append(" </list>\n"); 1717 } 1718 1719 public void visit(ListListSetting setting) { 1720 b.append(" <lists key='").append(XmlWriter.encode(key)).append("'>\n"); 1721 for (List<String> list : setting.getValue()) { 1722 b.append(" <list>\n"); 1723 for (String s : list) { 1724 b.append(" <entry value='").append(XmlWriter.encode(s)).append("'/>\n"); 1725 } 1726 b.append(" </list>\n"); 1727 } 1728 b.append(" </lists>\n"); 1729 } 1730 1731 public void visit(MapListSetting setting) { 1732 b.append(" <maps key='").append(XmlWriter.encode(key)).append("'>\n"); 1733 for (Map<String, String> struct : setting.getValue()) { 1734 b.append(" <map>\n"); 1735 for (Entry<String, String> e : struct.entrySet()) { 1736 b.append(" <tag key='").append(XmlWriter.encode(e.getKey())).append("' value='").append(XmlWriter.encode(e.getValue())).append("'/>\n"); 1737 } 1738 b.append(" </map>\n"); 1739 } 1740 b.append(" </maps>\n"); 1741 } 1742 } 1743 1744 public String toXML(boolean nopass) { 1745 StringBuilder b = new StringBuilder( 1746 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + 1747 "<preferences xmlns=\"http://josm.openstreetmap.de/preferences-1.0\" version=\""+ 1748 Version.getInstance().getVersion() + "\">\n"); 1749 SettingToXml toXml = new SettingToXml(b, nopass); 1750 Map<String, Setting> settings = new TreeMap<String, Setting>(); 1751 1752 for (Entry<String, String> e : properties.entrySet()) { 1753 settings.put(e.getKey(), new StringSetting(e.getValue())); 1754 } 1755 for (Entry<String, List<String>> e : collectionProperties.entrySet()) { 1756 settings.put(e.getKey(), new ListSetting(e.getValue())); 1757 } 1758 for (Entry<String, List<List<String>>> e : arrayProperties.entrySet()) { 1759 settings.put(e.getKey(), new ListListSetting(e.getValue())); 1760 } 1761 for (Entry<String, List<Map<String, String>>> e : listOfStructsProperties.entrySet()) { 1762 settings.put(e.getKey(), new MapListSetting(e.getValue())); 1763 } 1764 for (Entry<String, Setting> e : settings.entrySet()) { 1765 toXml.setKey(e.getKey()); 1766 e.getValue().visit(toXml); 1767 } 1768 b.append("</preferences>\n"); 1769 return b.toString(); 1770 } 1771 1772 /** 1773 * Removes obsolete preference settings. If you throw out a once-used preference 1774 * setting, add it to the list here with an expiry date (written as comment). If you 1775 * see something with an expiry date in the past, remove it from the list. 1776 */ 1777 public void removeObsolete() { 1778 String[] obsolete = { 1779 "gui.combobox.maximum-row-count", // 08/2012 - briefly introduced with #7917, can be removed end 2012 1780 "color.Imagery fade", // 08/2012 - wrong property caused by #6723, can be removed mid-2013 1781 }; 1782 for (String key : obsolete) { 1783 boolean removed = false; 1784 if(properties.containsKey(key)) { properties.remove(key); removed = true; } 1785 if(collectionProperties.containsKey(key)) { collectionProperties.remove(key); removed = true; } 1786 if(arrayProperties.containsKey(key)) { arrayProperties.remove(key); removed = true; } 1787 if(listOfStructsProperties.containsKey(key)) { listOfStructsProperties.remove(key); removed = true; } 1788 if(removed) 1789 System.out.println(tr("Preference setting {0} has been removed since it is no longer used.", key)); 1790 } 1791 } 1792 1793 public static boolean isEqual(Setting a, Setting b) { 1794 if (a==null && b==null) return true; 1795 if (a==null) return false; 1796 if (b==null) return false; 1797 if (a==b) return true; 1798 1799 if (a instanceof StringSetting) 1800 return (a.getValue().equals(b.getValue())); 1801 if (a instanceof ListSetting) 1802 return equalCollection((Collection<String>) a.getValue(), (Collection<String>) b.getValue()); 1803 if (a instanceof ListListSetting) 1804 return equalArray((Collection<Collection<String>>) a.getValue(), (Collection<List<String>>) b.getValue()); 1805 if (a instanceof MapListSetting) 1806 return equalListOfStructs((Collection<Map<String, String>>) a.getValue(), (Collection<Map<String, String>>) b.getValue()); 1807 return a.equals(b); 1808 } 1809 1810 }