source: josm/trunk/src/org/openstreetmap/josm/data/Preferences.java@ 8513

Last change on this file since 8513 was 8513, checked in by Don-vip, 9 years ago

checkstyle: blocks

  • Property svn:eol-style set to native
File size: 67.5 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.Color;
7import java.awt.Toolkit;
8import java.io.BufferedReader;
9import java.io.File;
10import java.io.FileOutputStream;
11import java.io.IOException;
12import java.io.InputStream;
13import java.io.OutputStreamWriter;
14import java.io.PrintWriter;
15import java.io.Reader;
16import java.io.StringReader;
17import java.io.StringWriter;
18import java.lang.annotation.Retention;
19import java.lang.annotation.RetentionPolicy;
20import java.lang.reflect.Field;
21import java.nio.charset.StandardCharsets;
22import java.nio.file.Files;
23import java.util.ArrayList;
24import java.util.Collection;
25import java.util.Collections;
26import java.util.HashMap;
27import java.util.HashSet;
28import java.util.Iterator;
29import java.util.LinkedHashMap;
30import java.util.LinkedList;
31import java.util.List;
32import java.util.Map;
33import java.util.Map.Entry;
34import java.util.Objects;
35import java.util.ResourceBundle;
36import java.util.Set;
37import java.util.SortedMap;
38import java.util.TreeMap;
39import java.util.concurrent.CopyOnWriteArrayList;
40import java.util.regex.Matcher;
41import java.util.regex.Pattern;
42
43import javax.json.Json;
44import javax.json.JsonObject;
45import javax.json.JsonObjectBuilder;
46import javax.json.JsonReader;
47import javax.json.JsonValue;
48import javax.json.JsonWriter;
49import javax.swing.JOptionPane;
50import javax.swing.UIManager;
51import javax.xml.XMLConstants;
52import javax.xml.stream.XMLInputFactory;
53import javax.xml.stream.XMLStreamConstants;
54import javax.xml.stream.XMLStreamException;
55import javax.xml.stream.XMLStreamReader;
56import javax.xml.transform.stream.StreamSource;
57import javax.xml.validation.Schema;
58import javax.xml.validation.SchemaFactory;
59import javax.xml.validation.Validator;
60
61import org.openstreetmap.josm.Main;
62import org.openstreetmap.josm.data.preferences.ColorProperty;
63import org.openstreetmap.josm.io.CachedFile;
64import org.openstreetmap.josm.io.OfflineAccessException;
65import org.openstreetmap.josm.io.OnlineResource;
66import org.openstreetmap.josm.io.XmlWriter;
67import org.openstreetmap.josm.tools.CheckParameterUtil;
68import org.openstreetmap.josm.tools.ColorHelper;
69import org.openstreetmap.josm.tools.I18n;
70import org.openstreetmap.josm.tools.Utils;
71import org.xml.sax.SAXException;
72
73/**
74 * This class holds all preferences for JOSM.
75 *
76 * Other classes can register their beloved properties here. All properties will be
77 * saved upon set-access.
78 *
79 * Each property is a key=setting pair, where key is a String and setting can be one of
80 * 4 types:
81 * string, list, list of lists and list of maps.
82 * In addition, each key has a unique default value that is set when the value is first
83 * accessed using one of the get...() methods. You can use the same preference
84 * key in different parts of the code, but the default value must be the same
85 * everywhere. A default value of null means, the setting has been requested, but
86 * no default value was set. This is used in advanced preferences to present a list
87 * off all possible settings.
88 *
89 * At the moment, you cannot put the empty string for string properties.
90 * put(key, "") means, the property is removed.
91 *
92 * @author imi
93 * @since 74
94 */
95public class Preferences {
96 /**
97 * Internal storage for the preference directory.
98 * Do not access this variable directly!
99 * @see #getPreferencesDirectory()
100 */
101 private File preferencesDir = null;
102
103 /**
104 * Internal storage for the cache directory.
105 */
106 private File cacheDir = null;
107
108 /**
109 * Internal storage for the user data directory.
110 */
111 private File userdataDir = null;
112
113 /**
114 * Determines if preferences file is saved each time a property is changed.
115 */
116 private boolean saveOnPut = true;
117
118 /**
119 * Maps the setting name to the current value of the setting.
120 * The map must not contain null as key or value. The mapped setting objects
121 * must not have a null value.
122 */
123 protected final SortedMap<String, Setting<?>> settingsMap = new TreeMap<>();
124
125 /**
126 * Maps the setting name to the default value of the setting.
127 * The map must not contain null as key or value. The value of the mapped
128 * setting objects can be null.
129 */
130 protected final SortedMap<String, Setting<?>> defaultsMap = new TreeMap<>();
131
132 /**
133 * Maps color keys to human readable color name
134 */
135 protected final SortedMap<String, String> colornames = new TreeMap<>();
136
137 /**
138 * Interface for a preference value.
139 *
140 * Implementations must provide a proper <code>equals</code> method.
141 *
142 * @param <T> the data type for the value
143 */
144 public interface Setting<T> {
145 /**
146 * Returns the value of this setting.
147 *
148 * @return the value of this setting
149 */
150 T getValue();
151
152 /**
153 * Check if the value of this Setting object is equal to the given value.
154 * @param otherVal the other value
155 * @return true if the values are equal
156 */
157 boolean equalVal(T otherVal);
158
159 /**
160 * Clone the current object.
161 * @return an identical copy of the current object
162 */
163 Setting<T> copy();
164
165 /**
166 * Enable usage of the visitor pattern.
167 *
168 * @param visitor the visitor
169 */
170 void visit(SettingVisitor visitor);
171
172 /**
173 * Returns a setting whose value is null.
174 *
175 * Cannot be static, because there is no static inheritance.
176 * @return a Setting object that isn't null itself, but returns null
177 * for {@link #getValue()}
178 */
179 Setting<T> getNullInstance();
180 }
181
182 /**
183 * Base abstract class of all settings, holding the setting value.
184 *
185 * @param <T> The setting type
186 */
187 public abstract static class AbstractSetting<T> implements Setting<T> {
188 protected final T value;
189 /**
190 * Constructs a new {@code AbstractSetting} with the given value
191 * @param value The setting value
192 */
193 public AbstractSetting(T value) {
194 this.value = value;
195 }
196
197 @Override
198 public T getValue() {
199 return value;
200 }
201
202 @Override
203 public String toString() {
204 return value != null ? value.toString() : "null";
205 }
206
207 @Override
208 public int hashCode() {
209 final int prime = 31;
210 int result = 1;
211 result = prime * result + ((value == null) ? 0 : value.hashCode());
212 return result;
213 }
214
215 @Override
216 public boolean equals(Object obj) {
217 if (this == obj)
218 return true;
219 if (obj == null)
220 return false;
221 if (!(obj instanceof AbstractSetting))
222 return false;
223 AbstractSetting<?> other = (AbstractSetting<?>) obj;
224 if (value == null) {
225 if (other.value != null)
226 return false;
227 } else if (!value.equals(other.value))
228 return false;
229 return true;
230 }
231 }
232
233 /**
234 * Setting containing a {@link String} value.
235 */
236 public static class StringSetting extends AbstractSetting<String> {
237 /**
238 * Constructs a new {@code StringSetting} with the given value
239 * @param value The setting value
240 */
241 public StringSetting(String value) {
242 super(value);
243 }
244
245 @Override
246 public boolean equalVal(String otherVal) {
247 if (value == null) return otherVal == null;
248 return value.equals(otherVal);
249 }
250
251 @Override
252 public StringSetting copy() {
253 return new StringSetting(value);
254 }
255
256 @Override
257 public void visit(SettingVisitor visitor) {
258 visitor.visit(this);
259 }
260
261 @Override
262 public StringSetting getNullInstance() {
263 return new StringSetting(null);
264 }
265
266 @Override
267 public boolean equals(Object other) {
268 if (!(other instanceof StringSetting)) return false;
269 return equalVal(((StringSetting) other).getValue());
270 }
271 }
272
273 /**
274 * Setting containing a {@link List} of {@link String} values.
275 */
276 public static class ListSetting extends AbstractSetting<List<String>> {
277 /**
278 * Constructs a new {@code ListSetting} with the given value
279 * @param value The setting value
280 */
281 public ListSetting(List<String> value) {
282 super(value);
283 consistencyTest();
284 }
285
286 /**
287 * Convenience factory method.
288 * @param value the value
289 * @return a corresponding ListSetting object
290 */
291 public static ListSetting create(Collection<String> value) {
292 return new ListSetting(value == null ? null : Collections.unmodifiableList(new ArrayList<>(value)));
293 }
294
295 @Override
296 public boolean equalVal(List<String> otherVal) {
297 return equalCollection(value, otherVal);
298 }
299
300 public static boolean equalCollection(Collection<String> a, Collection<String> b) {
301 if (a == null) return b == null;
302 if (b == null) return false;
303 if (a.size() != b.size()) return false;
304 Iterator<String> itA = a.iterator();
305 Iterator<String> itB = b.iterator();
306 while (itA.hasNext()) {
307 String aStr = itA.next();
308 String bStr = itB.next();
309 if (!Objects.equals(aStr, bStr)) return false;
310 }
311 return true;
312 }
313
314 @Override
315 public ListSetting copy() {
316 return ListSetting.create(value);
317 }
318
319 private void consistencyTest() {
320 if (value != null && value.contains(null))
321 throw new RuntimeException("Error: Null as list element in preference setting");
322 }
323
324 @Override
325 public void visit(SettingVisitor visitor) {
326 visitor.visit(this);
327 }
328
329 @Override
330 public ListSetting getNullInstance() {
331 return new ListSetting(null);
332 }
333
334 @Override
335 public boolean equals(Object other) {
336 if (!(other instanceof ListSetting)) return false;
337 return equalVal(((ListSetting) other).getValue());
338 }
339 }
340
341 /**
342 * Setting containing a {@link List} of {@code List}s of {@link String} values.
343 */
344 public static class ListListSetting extends AbstractSetting<List<List<String>>> {
345
346 /**
347 * Constructs a new {@code ListListSetting} with the given value
348 * @param value The setting value
349 */
350 public ListListSetting(List<List<String>> value) {
351 super(value);
352 consistencyTest();
353 }
354
355 /**
356 * Convenience factory method.
357 * @param value the value
358 * @return a corresponding ListListSetting object
359 */
360 public static ListListSetting create(Collection<Collection<String>> value) {
361 if (value != null) {
362 List<List<String>> valueList = new ArrayList<>(value.size());
363 for (Collection<String> lst : value) {
364 valueList.add(new ArrayList<>(lst));
365 }
366 return new ListListSetting(valueList);
367 }
368 return new ListListSetting(null);
369 }
370
371 @Override
372 public boolean equalVal(List<List<String>> otherVal) {
373 if (value == null) return otherVal == null;
374 if (otherVal == null) return false;
375 if (value.size() != otherVal.size()) return false;
376 Iterator<List<String>> itA = value.iterator();
377 Iterator<List<String>> itB = otherVal.iterator();
378 while (itA.hasNext()) {
379 if (!ListSetting.equalCollection(itA.next(), itB.next())) return false;
380 }
381 return true;
382 }
383
384 @Override
385 public ListListSetting copy() {
386 if (value == null) return new ListListSetting(null);
387
388 List<List<String>> copy = new ArrayList<>(value.size());
389 for (Collection<String> lst : value) {
390 List<String> lstCopy = new ArrayList<>(lst);
391 copy.add(Collections.unmodifiableList(lstCopy));
392 }
393 return new ListListSetting(Collections.unmodifiableList(copy));
394 }
395
396 private void consistencyTest() {
397 if (value == null) return;
398 if (value.contains(null)) throw new RuntimeException("Error: Null as list element in preference setting");
399 for (Collection<String> lst : value) {
400 if (lst.contains(null)) throw new RuntimeException("Error: Null as inner list element in preference setting");
401 }
402 }
403
404 @Override
405 public void visit(SettingVisitor visitor) {
406 visitor.visit(this);
407 }
408
409 @Override
410 public ListListSetting getNullInstance() {
411 return new ListListSetting(null);
412 }
413
414 @Override
415 public boolean equals(Object other) {
416 if (!(other instanceof ListListSetting)) return false;
417 return equalVal(((ListListSetting) other).getValue());
418 }
419 }
420
421 /**
422 * Setting containing a {@link List} of {@link Map}s of {@link String} values.
423 */
424 public static class MapListSetting extends AbstractSetting<List<Map<String, String>>> {
425
426 /**
427 * Constructs a new {@code MapListSetting} with the given value
428 * @param value The setting value
429 */
430 public MapListSetting(List<Map<String, String>> value) {
431 super(value);
432 consistencyTest();
433 }
434
435 @Override
436 public boolean equalVal(List<Map<String, String>> otherVal) {
437 if (value == null) return otherVal == null;
438 if (otherVal == null) return false;
439 if (value.size() != otherVal.size()) return false;
440 Iterator<Map<String, String>> itA = value.iterator();
441 Iterator<Map<String, String>> itB = otherVal.iterator();
442 while (itA.hasNext()) {
443 if (!equalMap(itA.next(), itB.next())) return false;
444 }
445 return true;
446 }
447
448 private static boolean equalMap(Map<String, String> a, Map<String, String> b) {
449 if (a == null) return b == null;
450 if (b == null) return false;
451 if (a.size() != b.size()) return false;
452 for (Entry<String, String> e : a.entrySet()) {
453 if (!Objects.equals(e.getValue(), b.get(e.getKey()))) return false;
454 }
455 return true;
456 }
457
458 @Override
459 public MapListSetting copy() {
460 if (value == null) return new MapListSetting(null);
461 List<Map<String, String>> copy = new ArrayList<>(value.size());
462 for (Map<String, String> map : value) {
463 Map<String, String> mapCopy = new LinkedHashMap<>(map);
464 copy.add(Collections.unmodifiableMap(mapCopy));
465 }
466 return new MapListSetting(Collections.unmodifiableList(copy));
467 }
468
469 private void consistencyTest() {
470 if (value == null) return;
471 if (value.contains(null)) throw new RuntimeException("Error: Null as list element in preference setting");
472 for (Map<String, String> map : value) {
473 if (map.keySet().contains(null)) throw new RuntimeException("Error: Null as map key in preference setting");
474 if (map.values().contains(null)) throw new RuntimeException("Error: Null as map value in preference setting");
475 }
476 }
477
478 @Override
479 public void visit(SettingVisitor visitor) {
480 visitor.visit(this);
481 }
482
483 @Override
484 public MapListSetting getNullInstance() {
485 return new MapListSetting(null);
486 }
487
488 @Override
489 public boolean equals(Object other) {
490 if (!(other instanceof MapListSetting)) return false;
491 return equalVal(((MapListSetting) other).getValue());
492 }
493 }
494
495 public interface SettingVisitor {
496 void visit(StringSetting setting);
497
498 void visit(ListSetting value);
499
500 void visit(ListListSetting value);
501
502 void visit(MapListSetting value);
503 }
504
505 public interface PreferenceChangeEvent {
506 String getKey();
507
508 Setting<?> getOldValue();
509
510 Setting<?> getNewValue();
511 }
512
513 public interface PreferenceChangedListener {
514 void preferenceChanged(PreferenceChangeEvent e);
515 }
516
517 private static class DefaultPreferenceChangeEvent implements PreferenceChangeEvent {
518 private final String key;
519 private final Setting<?> oldValue;
520 private final Setting<?> newValue;
521
522 public DefaultPreferenceChangeEvent(String key, Setting<?> oldValue, Setting<?> newValue) {
523 this.key = key;
524 this.oldValue = oldValue;
525 this.newValue = newValue;
526 }
527
528 @Override
529 public String getKey() {
530 return key;
531 }
532
533 @Override
534 public Setting<?> getOldValue() {
535 return oldValue;
536 }
537
538 @Override
539 public Setting<?> getNewValue() {
540 return newValue;
541 }
542 }
543
544 public interface ColorKey {
545 String getColorName();
546
547 String getSpecialName();
548
549 Color getDefaultValue();
550 }
551
552 private final CopyOnWriteArrayList<PreferenceChangedListener> listeners = new CopyOnWriteArrayList<>();
553
554 /**
555 * Adds a new preferences listener.
556 * @param listener The listener to add
557 */
558 public void addPreferenceChangeListener(PreferenceChangedListener listener) {
559 if (listener != null) {
560 listeners.addIfAbsent(listener);
561 }
562 }
563
564 /**
565 * Removes a preferences listener.
566 * @param listener The listener to remove
567 */
568 public void removePreferenceChangeListener(PreferenceChangedListener listener) {
569 listeners.remove(listener);
570 }
571
572 protected void firePreferenceChanged(String key, Setting<?> oldValue, Setting<?> newValue) {
573 PreferenceChangeEvent evt = new DefaultPreferenceChangeEvent(key, oldValue, newValue);
574 for (PreferenceChangedListener l : listeners) {
575 l.preferenceChanged(evt);
576 }
577 }
578
579 /**
580 * Returns the user defined preferences directory, containing the preferences.xml file
581 * @return The user defined preferences directory, containing the preferences.xml file
582 * @since 7834
583 */
584 public File getPreferencesDirectory() {
585 if (preferencesDir != null)
586 return preferencesDir;
587 String path;
588 path = System.getProperty("josm.pref");
589 if (path != null) {
590 preferencesDir = new File(path).getAbsoluteFile();
591 } else {
592 path = System.getProperty("josm.home");
593 if (path != null) {
594 preferencesDir = new File(path).getAbsoluteFile();
595 } else {
596 preferencesDir = Main.platform.getDefaultPrefDirectory();
597 }
598 }
599 return preferencesDir;
600 }
601
602 /**
603 * Returns the user data directory, containing autosave, plugins, etc.
604 * Depending on the OS it may be the same directory as preferences directory.
605 * @return The user data directory, containing autosave, plugins, etc.
606 * @since 7834
607 */
608 public File getUserDataDirectory() {
609 if (userdataDir != null)
610 return userdataDir;
611 String path;
612 path = System.getProperty("josm.userdata");
613 if (path != null) {
614 userdataDir = new File(path).getAbsoluteFile();
615 } else {
616 path = System.getProperty("josm.home");
617 if (path != null) {
618 userdataDir = new File(path).getAbsoluteFile();
619 } else {
620 userdataDir = Main.platform.getDefaultUserDataDirectory();
621 }
622 }
623 return userdataDir;
624 }
625
626 /**
627 * Returns the user preferences file (preferences.xml)
628 * @return The user preferences file (preferences.xml)
629 */
630 public File getPreferenceFile() {
631 return new File(getPreferencesDirectory(), "preferences.xml");
632 }
633
634 /**
635 * Returns the user plugin directory
636 * @return The user plugin directory
637 */
638 public File getPluginsDirectory() {
639 return new File(getUserDataDirectory(), "plugins");
640 }
641
642 /**
643 * Get the directory where cached content of any kind should be stored.
644 *
645 * If the directory doesn't exist on the file system, it will be created
646 * by this method.
647 *
648 * @return the cache directory
649 */
650 public File getCacheDirectory() {
651 if (cacheDir != null)
652 return cacheDir;
653 String path = System.getProperty("josm.cache");
654 if (path != null) {
655 cacheDir = new File(path).getAbsoluteFile();
656 } else {
657 path = System.getProperty("josm.home");
658 if (path != null) {
659 cacheDir = new File(path, "cache");
660 } else {
661 path = get("cache.folder", null);
662 if (path != null) {
663 cacheDir = new File(path).getAbsoluteFile();
664 } else {
665 cacheDir = Main.platform.getDefaultCacheDirectory();
666 }
667 }
668 }
669 if (!cacheDir.exists() && !cacheDir.mkdirs()) {
670 Main.warn(tr("Failed to create missing cache directory: {0}", cacheDir.getAbsoluteFile()));
671 JOptionPane.showMessageDialog(
672 Main.parent,
673 tr("<html>Failed to create missing cache directory: {0}</html>", cacheDir.getAbsoluteFile()),
674 tr("Error"),
675 JOptionPane.ERROR_MESSAGE
676 );
677 }
678 return cacheDir;
679 }
680
681 private void addPossibleResourceDir(Set<String> locations, String s) {
682 if (s != null) {
683 if (!s.endsWith(File.separator)) {
684 s += File.separator;
685 }
686 locations.add(s);
687 }
688 }
689
690 /**
691 * Returns a set of all existing directories where resources could be stored.
692 * @return A set of all existing directories where resources could be stored.
693 */
694 public Collection<String> getAllPossiblePreferenceDirs() {
695 Set<String> locations = new HashSet<>();
696 addPossibleResourceDir(locations, getPreferencesDirectory().getPath());
697 addPossibleResourceDir(locations, getUserDataDirectory().getPath());
698 addPossibleResourceDir(locations, System.getenv("JOSM_RESOURCES"));
699 addPossibleResourceDir(locations, System.getProperty("josm.resources"));
700 if (Main.isPlatformWindows()) {
701 String appdata = System.getenv("APPDATA");
702 if (System.getenv("ALLUSERSPROFILE") != null && appdata != null
703 && appdata.lastIndexOf(File.separator) != -1) {
704 appdata = appdata.substring(appdata.lastIndexOf(File.separator));
705 locations.add(new File(new File(System.getenv("ALLUSERSPROFILE"),
706 appdata), "JOSM").getPath());
707 }
708 } else {
709 locations.add("/usr/local/share/josm/");
710 locations.add("/usr/local/lib/josm/");
711 locations.add("/usr/share/josm/");
712 locations.add("/usr/lib/josm/");
713 }
714 return locations;
715 }
716
717 /**
718 * Get settings value for a certain key.
719 * @param key the identifier for the setting
720 * @return "" if there is nothing set for the preference key,
721 * the corresponding value otherwise. The result is not null.
722 */
723 public synchronized String get(final String key) {
724 String value = get(key, null);
725 return value == null ? "" : value;
726 }
727
728 /**
729 * Get settings value for a certain key and provide default a value.
730 * @param key the identifier for the setting
731 * @param def the default value. For each call of get() with a given key, the
732 * default value must be the same.
733 * @return the corresponding value if the property has been set before,
734 * def otherwise
735 */
736 public synchronized String get(final String key, final String def) {
737 return getSetting(key, new StringSetting(def), StringSetting.class).getValue();
738 }
739
740 public synchronized Map<String, String> getAllPrefix(final String prefix) {
741 final Map<String, String> all = new TreeMap<>();
742 for (final Entry<String, Setting<?>> e : settingsMap.entrySet()) {
743 if (e.getKey().startsWith(prefix) && (e.getValue() instanceof StringSetting)) {
744 all.put(e.getKey(), ((StringSetting) e.getValue()).getValue());
745 }
746 }
747 return all;
748 }
749
750 public synchronized List<String> getAllPrefixCollectionKeys(final String prefix) {
751 final List<String> all = new LinkedList<>();
752 for (Map.Entry<String, Setting<?>> entry : settingsMap.entrySet()) {
753 if (entry.getKey().startsWith(prefix) && entry.getValue() instanceof ListSetting) {
754 all.add(entry.getKey());
755 }
756 }
757 return all;
758 }
759
760 public synchronized Map<String, String> getAllColors() {
761 final Map<String, String> all = new TreeMap<>();
762 for (final Entry<String, Setting<?>> e : defaultsMap.entrySet()) {
763 if (e.getKey().startsWith("color.") && e.getValue() instanceof StringSetting) {
764 StringSetting d = (StringSetting) e.getValue();
765 if (d.getValue() != null) {
766 all.put(e.getKey().substring(6), d.getValue());
767 }
768 }
769 }
770 for (final Entry<String, Setting<?>> e : settingsMap.entrySet()) {
771 if (e.getKey().startsWith("color.") && (e.getValue() instanceof StringSetting)) {
772 all.put(e.getKey().substring(6), ((StringSetting) e.getValue()).getValue());
773 }
774 }
775 return all;
776 }
777
778 public synchronized boolean getBoolean(final String key) {
779 String s = get(key, null);
780 return s == null ? false : Boolean.parseBoolean(s);
781 }
782
783 public synchronized boolean getBoolean(final String key, final boolean def) {
784 return Boolean.parseBoolean(get(key, Boolean.toString(def)));
785 }
786
787 public synchronized boolean getBoolean(final String key, final String specName, final boolean def) {
788 boolean generic = getBoolean(key, def);
789 String skey = key+"."+specName;
790 Setting<?> prop = settingsMap.get(skey);
791 if (prop instanceof StringSetting)
792 return Boolean.parseBoolean(((StringSetting) prop).getValue());
793 else
794 return generic;
795 }
796
797 /**
798 * Set a value for a certain setting.
799 * @param key the unique identifier for the setting
800 * @param value the value of the setting. Can be null or "" which both removes
801 * the key-value entry.
802 * @return true, if something has changed (i.e. value is different than before)
803 */
804 public boolean put(final String key, String value) {
805 if (value != null && value.isEmpty()) {
806 value = null;
807 }
808 return putSetting(key, value == null ? null : new StringSetting(value));
809 }
810
811 public boolean put(final String key, final boolean value) {
812 return put(key, Boolean.toString(value));
813 }
814
815 public boolean putInteger(final String key, final Integer value) {
816 return put(key, Integer.toString(value));
817 }
818
819 public boolean putDouble(final String key, final Double value) {
820 return put(key, Double.toString(value));
821 }
822
823 public boolean putLong(final String key, final Long value) {
824 return put(key, Long.toString(value));
825 }
826
827 /**
828 * Called after every put. In case of a problem, do nothing but output the error in log.
829 */
830 public void save() throws IOException {
831 /* currently unused, but may help to fix configuration issues in future */
832 putInteger("josm.version", Version.getInstance().getVersion());
833
834 updateSystemProperties();
835
836 File prefFile = getPreferenceFile();
837 File backupFile = new File(prefFile + "_backup");
838
839 // Backup old preferences if there are old preferences
840 if (prefFile.exists()) {
841 Utils.copyFile(prefFile, backupFile);
842 }
843
844 try (PrintWriter out = new PrintWriter(new OutputStreamWriter(
845 new FileOutputStream(prefFile + "_tmp"), StandardCharsets.UTF_8), false)) {
846 out.print(toXML(false));
847 }
848
849 File tmpFile = new File(prefFile + "_tmp");
850 Utils.copyFile(tmpFile, prefFile);
851 if (!tmpFile.delete()) {
852 Main.warn(tr("Unable to delete temporary file {0}", tmpFile.getAbsolutePath()));
853 }
854
855 setCorrectPermissions(prefFile);
856 setCorrectPermissions(backupFile);
857 }
858
859 private void setCorrectPermissions(File file) {
860 if (!file.setReadable(false, false) && Main.isDebugEnabled()) {
861 Main.debug(tr("Unable to set file non-readable {0}", file.getAbsolutePath()));
862 }
863 if (!file.setWritable(false, false) && Main.isDebugEnabled()) {
864 Main.debug(tr("Unable to set file non-writable {0}", file.getAbsolutePath()));
865 }
866 if (!file.setExecutable(false, false) && Main.isDebugEnabled()) {
867 Main.debug(tr("Unable to set file non-executable {0}", file.getAbsolutePath()));
868 }
869 if (!file.setReadable(true, true) && Main.isDebugEnabled()) {
870 Main.debug(tr("Unable to set file readable {0}", file.getAbsolutePath()));
871 }
872 if (!file.setWritable(true, true) && Main.isDebugEnabled()) {
873 Main.debug(tr("Unable to set file writable {0}", file.getAbsolutePath()));
874 }
875 }
876
877 /**
878 * Loads preferences from settings file.
879 * @throws IOException if any I/O error occurs while reading the file
880 * @throws SAXException if the settings file does not contain valid XML
881 * @throws XMLStreamException if an XML error occurs while parsing the file (after validation)
882 */
883 public void load() throws IOException, SAXException, XMLStreamException {
884 settingsMap.clear();
885 File pref = getPreferenceFile();
886 try (BufferedReader in = Files.newBufferedReader(pref.toPath(), StandardCharsets.UTF_8)) {
887 validateXML(in);
888 }
889 try (BufferedReader in = Files.newBufferedReader(pref.toPath(), StandardCharsets.UTF_8)) {
890 fromXML(in);
891 }
892 updateSystemProperties();
893 removeObsolete();
894 }
895
896 /**
897 * Initializes preferences.
898 * @param reset if {@code true}, current settings file is replaced by the default one
899 */
900 public void init(boolean reset) {
901 // get the preferences.
902 File prefDir = getPreferencesDirectory();
903 if (prefDir.exists()) {
904 if (!prefDir.isDirectory()) {
905 Main.warn(tr("Failed to initialize preferences. Preference directory ''{0}'' is not a directory.",
906 prefDir.getAbsoluteFile()));
907 JOptionPane.showMessageDialog(
908 Main.parent,
909 tr("<html>Failed to initialize preferences.<br>Preference directory ''{0}'' is not a directory.</html>",
910 prefDir.getAbsoluteFile()),
911 tr("Error"),
912 JOptionPane.ERROR_MESSAGE
913 );
914 return;
915 }
916 } else {
917 if (!prefDir.mkdirs()) {
918 Main.warn(tr("Failed to initialize preferences. Failed to create missing preference directory: {0}",
919 prefDir.getAbsoluteFile()));
920 JOptionPane.showMessageDialog(
921 Main.parent,
922 tr("<html>Failed to initialize preferences.<br>Failed to create missing preference directory: {0}</html>",
923 prefDir.getAbsoluteFile()),
924 tr("Error"),
925 JOptionPane.ERROR_MESSAGE
926 );
927 return;
928 }
929 }
930
931 File preferenceFile = getPreferenceFile();
932 try {
933 if (!preferenceFile.exists()) {
934 Main.info(tr("Missing preference file ''{0}''. Creating a default preference file.", preferenceFile.getAbsoluteFile()));
935 resetToDefault();
936 save();
937 } else if (reset) {
938 Main.warn(tr("Replacing existing preference file ''{0}'' with default preference file.", preferenceFile.getAbsoluteFile()));
939 resetToDefault();
940 save();
941 }
942 } catch (IOException e) {
943 Main.error(e);
944 JOptionPane.showMessageDialog(
945 Main.parent,
946 tr("<html>Failed to initialize preferences.<br>Failed to reset preference file to default: {0}</html>",
947 getPreferenceFile().getAbsoluteFile()),
948 tr("Error"),
949 JOptionPane.ERROR_MESSAGE
950 );
951 return;
952 }
953 try {
954 load();
955 } catch (Exception e) {
956 Main.error(e);
957 File backupFile = new File(prefDir, "preferences.xml.bak");
958 JOptionPane.showMessageDialog(
959 Main.parent,
960 tr("<html>Preferences file had errors.<br> Making backup of old one to <br>{0}<br> " +
961 "and creating a new default preference file.</html>",
962 backupFile.getAbsoluteFile()),
963 tr("Error"),
964 JOptionPane.ERROR_MESSAGE
965 );
966 Main.platform.rename(preferenceFile, backupFile);
967 try {
968 resetToDefault();
969 save();
970 } catch (IOException e1) {
971 Main.error(e1);
972 Main.warn(tr("Failed to initialize preferences. Failed to reset preference file to default: {0}", getPreferenceFile()));
973 }
974 }
975 }
976
977 public final void resetToDefault() {
978 settingsMap.clear();
979 }
980
981 /**
982 * Convenience method for accessing colour preferences.
983 *
984 * @param colName name of the colour
985 * @param def default value
986 * @return a Color object for the configured colour, or the default value if none configured.
987 */
988 public synchronized Color getColor(String colName, Color def) {
989 return getColor(colName, null, def);
990 }
991
992 public synchronized Color getUIColor(String colName) {
993 return UIManager.getColor(colName);
994 }
995
996 /* only for preferences */
997 public synchronized String getColorName(String o) {
998 try {
999 Matcher m = Pattern.compile("mappaint\\.(.+?)\\.(.+)").matcher(o);
1000 if (m.matches()) {
1001 return tr("Paint style {0}: {1}", tr(I18n.escape(m.group(1))), tr(I18n.escape(m.group(2))));
1002 }
1003 } catch (Exception e) {
1004 Main.warn(e);
1005 }
1006 try {
1007 Matcher m = Pattern.compile("layer (.+)").matcher(o);
1008 if (m.matches()) {
1009 return tr("Layer: {0}", tr(I18n.escape(m.group(1))));
1010 }
1011 } catch (Exception e) {
1012 Main.warn(e);
1013 }
1014 return tr(I18n.escape(colornames.containsKey(o) ? colornames.get(o) : o));
1015 }
1016
1017 /**
1018 * Returns the color for the given key.
1019 * @param key The color key
1020 * @return the color
1021 */
1022 public Color getColor(ColorKey key) {
1023 return getColor(key.getColorName(), key.getSpecialName(), key.getDefaultValue());
1024 }
1025
1026 /**
1027 * Convenience method for accessing colour preferences.
1028 *
1029 * @param colName name of the colour
1030 * @param specName name of the special colour settings
1031 * @param def default value
1032 * @return a Color object for the configured colour, or the default value if none configured.
1033 */
1034 public synchronized Color getColor(String colName, String specName, Color def) {
1035 String colKey = ColorProperty.getColorKey(colName);
1036 if (!colKey.equals(colName)) {
1037 colornames.put(colKey, colName);
1038 }
1039 String colStr = specName != null ? get("color."+specName) : "";
1040 if (colStr.isEmpty()) {
1041 colStr = get("color." + colKey, ColorHelper.color2html(def, true));
1042 }
1043 if (colStr != null && !colStr.isEmpty()) {
1044 return ColorHelper.html2color(colStr);
1045 } else {
1046 return def;
1047 }
1048 }
1049
1050 public synchronized Color getDefaultColor(String colKey) {
1051 StringSetting col = Utils.cast(defaultsMap.get("color."+colKey), StringSetting.class);
1052 String colStr = col == null ? null : col.getValue();
1053 return colStr == null || colStr.isEmpty() ? null : ColorHelper.html2color(colStr);
1054 }
1055
1056 public synchronized boolean putColor(String colKey, Color val) {
1057 return put("color."+colKey, val != null ? ColorHelper.color2html(val, true) : null);
1058 }
1059
1060 public synchronized int getInteger(String key, int def) {
1061 String v = get(key, Integer.toString(def));
1062 if (v.isEmpty())
1063 return def;
1064
1065 try {
1066 return Integer.parseInt(v);
1067 } catch (NumberFormatException e) {
1068 // fall out
1069 if (Main.isTraceEnabled()) {
1070 Main.trace(e.getMessage());
1071 }
1072 }
1073 return def;
1074 }
1075
1076 public synchronized int getInteger(String key, String specName, int def) {
1077 String v = get(key+"."+specName);
1078 if (v.isEmpty())
1079 v = get(key, Integer.toString(def));
1080 if (v.isEmpty())
1081 return def;
1082
1083 try {
1084 return Integer.parseInt(v);
1085 } catch (NumberFormatException e) {
1086 // fall out
1087 if (Main.isTraceEnabled()) {
1088 Main.trace(e.getMessage());
1089 }
1090 }
1091 return def;
1092 }
1093
1094 public synchronized long getLong(String key, long def) {
1095 String v = get(key, Long.toString(def));
1096 if (null == v)
1097 return def;
1098
1099 try {
1100 return Long.parseLong(v);
1101 } catch (NumberFormatException e) {
1102 // fall out
1103 if (Main.isTraceEnabled()) {
1104 Main.trace(e.getMessage());
1105 }
1106 }
1107 return def;
1108 }
1109
1110 public synchronized double getDouble(String key, double def) {
1111 String v = get(key, Double.toString(def));
1112 if (null == v)
1113 return def;
1114
1115 try {
1116 return Double.parseDouble(v);
1117 } catch (NumberFormatException e) {
1118 // fall out
1119 if (Main.isTraceEnabled()) {
1120 Main.trace(e.getMessage());
1121 }
1122 }
1123 return def;
1124 }
1125
1126 /**
1127 * Get a list of values for a certain key
1128 * @param key the identifier for the setting
1129 * @param def the default value.
1130 * @return the corresponding value if the property has been set before,
1131 * def otherwise
1132 */
1133 public Collection<String> getCollection(String key, Collection<String> def) {
1134 return getSetting(key, ListSetting.create(def), ListSetting.class).getValue();
1135 }
1136
1137 /**
1138 * Get a list of values for a certain key
1139 * @param key the identifier for the setting
1140 * @return the corresponding value if the property has been set before,
1141 * an empty Collection otherwise.
1142 */
1143 public Collection<String> getCollection(String key) {
1144 Collection<String> val = getCollection(key, null);
1145 return val == null ? Collections.<String>emptyList() : val;
1146 }
1147
1148 public synchronized void removeFromCollection(String key, String value) {
1149 List<String> a = new ArrayList<>(getCollection(key, Collections.<String>emptyList()));
1150 a.remove(value);
1151 putCollection(key, a);
1152 }
1153
1154 /**
1155 * Set a value for a certain setting. The changed setting is saved
1156 * to the preference file immediately. Due to caching mechanisms on modern
1157 * operating systems and hardware, this shouldn't be a performance problem.
1158 * @param key the unique identifier for the setting
1159 * @param setting the value of the setting. In case it is null, the key-value
1160 * entry will be removed.
1161 * @return true, if something has changed (i.e. value is different than before)
1162 */
1163 public boolean putSetting(final String key, Setting<?> setting) {
1164 CheckParameterUtil.ensureParameterNotNull(key);
1165 if (setting != null && setting.getValue() == null)
1166 throw new IllegalArgumentException("setting argument must not have null value");
1167 Setting<?> settingOld;
1168 Setting<?> settingCopy = null;
1169 synchronized (this) {
1170 if (setting == null) {
1171 settingOld = settingsMap.remove(key);
1172 if (settingOld == null)
1173 return false;
1174 } else {
1175 settingOld = settingsMap.get(key);
1176 if (setting.equals(settingOld))
1177 return false;
1178 if (settingOld == null && setting.equals(defaultsMap.get(key)))
1179 return false;
1180 settingCopy = setting.copy();
1181 settingsMap.put(key, settingCopy);
1182 }
1183 if (saveOnPut) {
1184 try {
1185 save();
1186 } catch (IOException e) {
1187 Main.warn(tr("Failed to persist preferences to ''{0}''", getPreferenceFile().getAbsoluteFile()));
1188 }
1189 }
1190 }
1191 // Call outside of synchronized section in case some listener wait for other thread that wait for preference lock
1192 firePreferenceChanged(key, settingOld, settingCopy);
1193 return true;
1194 }
1195
1196 public synchronized Setting<?> getSetting(String key, Setting<?> def) {
1197 return getSetting(key, def, Setting.class);
1198 }
1199
1200 /**
1201 * Get settings value for a certain key and provide default a value.
1202 * @param <T> the setting type
1203 * @param key the identifier for the setting
1204 * @param def the default value. For each call of getSetting() with a given
1205 * key, the default value must be the same. <code>def</code> must not be
1206 * null, but the value of <code>def</code> can be null.
1207 * @param klass the setting type (same as T)
1208 * @return the corresponding value if the property has been set before,
1209 * def otherwise
1210 */
1211 @SuppressWarnings("unchecked")
1212 public synchronized <T extends Setting<?>> T getSetting(String key, T def, Class<T> klass) {
1213 CheckParameterUtil.ensureParameterNotNull(key);
1214 CheckParameterUtil.ensureParameterNotNull(def);
1215 Setting<?> oldDef = defaultsMap.get(key);
1216 if (oldDef != null && oldDef.getValue() != null && def.getValue() != null && !def.equals(oldDef)) {
1217 Main.info("Defaults for " + key + " differ: " + def + " != " + defaultsMap.get(key));
1218 }
1219 if (def.getValue() != null || oldDef == null) {
1220 defaultsMap.put(key, def.copy());
1221 }
1222 Setting<?> prop = settingsMap.get(key);
1223 if (klass.isInstance(prop)) {
1224 return (T) prop;
1225 } else {
1226 return def;
1227 }
1228 }
1229
1230 public boolean putCollection(String key, Collection<String> value) {
1231 return putSetting(key, value == null ? null : ListSetting.create(value));
1232 }
1233
1234 /**
1235 * Saves at most {@code maxsize} items of collection {@code val}.
1236 */
1237 public boolean putCollectionBounded(String key, int maxsize, Collection<String> val) {
1238 Collection<String> newCollection = new ArrayList<>(Math.min(maxsize, val.size()));
1239 for (String i : val) {
1240 if (newCollection.size() >= maxsize) {
1241 break;
1242 }
1243 newCollection.add(i);
1244 }
1245 return putCollection(key, newCollection);
1246 }
1247
1248 /**
1249 * Used to read a 2-dimensional array of strings from the preference file.
1250 * If not a single entry could be found, <code>def</code> is returned.
1251 */
1252 @SuppressWarnings({ "unchecked", "rawtypes" })
1253 public synchronized Collection<Collection<String>> getArray(String key, Collection<Collection<String>> def) {
1254 ListListSetting val = getSetting(key, ListListSetting.create(def), ListListSetting.class);
1255 return (Collection) val.getValue();
1256 }
1257
1258 public Collection<Collection<String>> getArray(String key) {
1259 Collection<Collection<String>> res = getArray(key, null);
1260 return res == null ? Collections.<Collection<String>>emptyList() : res;
1261 }
1262
1263 public boolean putArray(String key, Collection<Collection<String>> value) {
1264 return putSetting(key, value == null ? null : ListListSetting.create(value));
1265 }
1266
1267 public Collection<Map<String, String>> getListOfStructs(String key, Collection<Map<String, String>> def) {
1268 return getSetting(key, new MapListSetting(def == null ? null : new ArrayList<>(def)), MapListSetting.class).getValue();
1269 }
1270
1271 public boolean putListOfStructs(String key, Collection<Map<String, String>> value) {
1272 return putSetting(key, value == null ? null : new MapListSetting(new ArrayList<>(value)));
1273 }
1274
1275 @Retention(RetentionPolicy.RUNTIME) public @interface pref { }
1276 @Retention(RetentionPolicy.RUNTIME) public @interface writeExplicitly { }
1277
1278 /**
1279 * Get a list of hashes which are represented by a struct-like class.
1280 * Possible properties are given by fields of the class klass that have
1281 * the @pref annotation.
1282 * Default constructor is used to initialize the struct objects, properties
1283 * then override some of these default values.
1284 * @param key main preference key
1285 * @param klass The struct class
1286 * @return a list of objects of type T or an empty list if nothing was found
1287 */
1288 public <T> List<T> getListOfStructs(String key, Class<T> klass) {
1289 List<T> r = getListOfStructs(key, null, klass);
1290 if (r == null)
1291 return Collections.emptyList();
1292 else
1293 return r;
1294 }
1295
1296 /**
1297 * same as above, but returns def if nothing was found
1298 */
1299 public <T> List<T> getListOfStructs(String key, Collection<T> def, Class<T> klass) {
1300 Collection<Map<String, String>> prop =
1301 getListOfStructs(key, def == null ? null : serializeListOfStructs(def, klass));
1302 if (prop == null)
1303 return def == null ? null : new ArrayList<>(def);
1304 List<T> lst = new ArrayList<>();
1305 for (Map<String, String> entries : prop) {
1306 T struct = deserializeStruct(entries, klass);
1307 lst.add(struct);
1308 }
1309 return lst;
1310 }
1311
1312 /**
1313 * Save a list of hashes represented by a struct-like class.
1314 * Considers only fields that have the @pref annotation.
1315 * In addition it does not write fields with null values. (Thus they are cleared)
1316 * Default values are given by the field values after default constructor has
1317 * been called.
1318 * Fields equal to the default value are not written unless the field has
1319 * the @writeExplicitly annotation.
1320 * @param key main preference key
1321 * @param val the list that is supposed to be saved
1322 * @param klass The struct class
1323 * @return true if something has changed
1324 */
1325 public <T> boolean putListOfStructs(String key, Collection<T> val, Class<T> klass) {
1326 return putListOfStructs(key, serializeListOfStructs(val, klass));
1327 }
1328
1329 private <T> Collection<Map<String, String>> serializeListOfStructs(Collection<T> l, Class<T> klass) {
1330 if (l == null)
1331 return null;
1332 Collection<Map<String, String>> vals = new ArrayList<>();
1333 for (T struct : l) {
1334 if (struct == null) {
1335 continue;
1336 }
1337 vals.add(serializeStruct(struct, klass));
1338 }
1339 return vals;
1340 }
1341
1342 @SuppressWarnings("rawtypes")
1343 private static String mapToJson(Map map) {
1344 StringWriter stringWriter = new StringWriter();
1345 try (JsonWriter writer = Json.createWriter(stringWriter)) {
1346 JsonObjectBuilder object = Json.createObjectBuilder();
1347 for (Object o: map.entrySet()) {
1348 Entry e = (Entry) o;
1349 object.add(e.getKey().toString(), e.getValue().toString());
1350 }
1351 writer.writeObject(object.build());
1352 }
1353 return stringWriter.toString();
1354 }
1355
1356 @SuppressWarnings({ "rawtypes", "unchecked" })
1357 private static Map mapFromJson(String s) {
1358 Map ret = null;
1359 try (JsonReader reader = Json.createReader(new StringReader(s))) {
1360 JsonObject object = reader.readObject();
1361 ret = new HashMap(object.size());
1362 for (Entry<String, JsonValue> e: object.entrySet()) {
1363 ret.put(e.getKey(), e.getValue().toString());
1364 }
1365 }
1366 return ret;
1367 }
1368
1369 public static <T> Map<String, String> serializeStruct(T struct, Class<T> klass) {
1370 T structPrototype;
1371 try {
1372 structPrototype = klass.newInstance();
1373 } catch (InstantiationException | IllegalAccessException ex) {
1374 throw new RuntimeException(ex);
1375 }
1376
1377 Map<String, String> hash = new LinkedHashMap<>();
1378 for (Field f : klass.getDeclaredFields()) {
1379 if (f.getAnnotation(pref.class) == null) {
1380 continue;
1381 }
1382 f.setAccessible(true);
1383 try {
1384 Object fieldValue = f.get(struct);
1385 Object defaultFieldValue = f.get(structPrototype);
1386 if (fieldValue != null) {
1387 if (f.getAnnotation(writeExplicitly.class) != null || !Objects.equals(fieldValue, defaultFieldValue)) {
1388 String key = f.getName().replace("_", "-");
1389 if (fieldValue instanceof Map) {
1390 hash.put(key, mapToJson((Map) fieldValue));
1391 } else {
1392 hash.put(key, fieldValue.toString());
1393 }
1394 }
1395 }
1396 } catch (IllegalArgumentException | IllegalAccessException ex) {
1397 throw new RuntimeException(ex);
1398 }
1399 }
1400 return hash;
1401 }
1402
1403 public static <T> T deserializeStruct(Map<String, String> hash, Class<T> klass) {
1404 T struct = null;
1405 try {
1406 struct = klass.newInstance();
1407 } catch (InstantiationException | IllegalAccessException ex) {
1408 throw new RuntimeException(ex);
1409 }
1410 for (Entry<String, String> key_value : hash.entrySet()) {
1411 Object value = null;
1412 Field f;
1413 try {
1414 f = klass.getDeclaredField(key_value.getKey().replace("-", "_"));
1415 } catch (NoSuchFieldException ex) {
1416 continue;
1417 } catch (SecurityException ex) {
1418 throw new RuntimeException(ex);
1419 }
1420 if (f.getAnnotation(pref.class) == null) {
1421 continue;
1422 }
1423 f.setAccessible(true);
1424 if (f.getType() == Boolean.class || f.getType() == boolean.class) {
1425 value = Boolean.valueOf(key_value.getValue());
1426 } else if (f.getType() == Integer.class || f.getType() == int.class) {
1427 try {
1428 value = Integer.valueOf(key_value.getValue());
1429 } catch (NumberFormatException nfe) {
1430 continue;
1431 }
1432 } else if (f.getType() == Double.class || f.getType() == double.class) {
1433 try {
1434 value = Double.valueOf(key_value.getValue());
1435 } catch (NumberFormatException nfe) {
1436 continue;
1437 }
1438 } else if (f.getType() == String.class) {
1439 value = key_value.getValue();
1440 } else if (f.getType().isAssignableFrom(Map.class)) {
1441 value = mapFromJson(key_value.getValue());
1442 } else
1443 throw new RuntimeException("unsupported preference primitive type");
1444
1445 try {
1446 f.set(struct, value);
1447 } catch (IllegalArgumentException ex) {
1448 throw new AssertionError(ex);
1449 } catch (IllegalAccessException ex) {
1450 throw new RuntimeException(ex);
1451 }
1452 }
1453 return struct;
1454 }
1455
1456 public Map<String, Setting<?>> getAllSettings() {
1457 return new TreeMap<>(settingsMap);
1458 }
1459
1460 public Map<String, Setting<?>> getAllDefaults() {
1461 return new TreeMap<>(defaultsMap);
1462 }
1463
1464 /**
1465 * Updates system properties with the current values in the preferences.
1466 *
1467 */
1468 public void updateSystemProperties() {
1469 if ("true".equals(get("prefer.ipv6", "auto"))) {
1470 // never set this to false, only true!
1471 if (!"true".equals(Utils.updateSystemProperty("java.net.preferIPv6Addresses", "true"))) {
1472 Main.info(tr("Try enabling IPv6 network, prefering IPv6 over IPv4 (only works on early startup)."));
1473 }
1474 }
1475 Utils.updateSystemProperty("http.agent", Version.getInstance().getAgentString());
1476 Utils.updateSystemProperty("user.language", get("language"));
1477 // Workaround to fix a Java bug.
1478 // Force AWT toolkit to update its internal preferences (fix #3645).
1479 // This ugly hack comes from Sun bug database: https://bugs.openjdk.java.net/browse/JDK-6292739
1480 try {
1481 Field field = Toolkit.class.getDeclaredField("resources");
1482 field.setAccessible(true);
1483 field.set(null, ResourceBundle.getBundle("sun.awt.resources.awt"));
1484 } catch (Exception e) {
1485 // Ignore all exceptions
1486 if (Main.isTraceEnabled()) {
1487 Main.trace(e.getMessage());
1488 }
1489 }
1490 // Workaround to fix a Java "feature"
1491 // See http://stackoverflow.com/q/7615645/2257172 and #9875
1492 if (getBoolean("jdk.tls.disableSNIExtension", true)) {
1493 Utils.updateSystemProperty("jsse.enableSNIExtension", "false");
1494 }
1495 // Workaround to fix another Java bug
1496 // Force Java 7 to use old sorting algorithm of Arrays.sort (fix #8712).
1497 // See Oracle bug database: https://bugs.openjdk.java.net/browse/JDK-7075600
1498 // and https://bugs.openjdk.java.net/browse/JDK-6923200
1499 if (getBoolean("jdk.Arrays.useLegacyMergeSort", !Version.getInstance().isLocalBuild())) {
1500 Utils.updateSystemProperty("java.util.Arrays.useLegacyMergeSort", "true");
1501 }
1502 }
1503
1504 /**
1505 * Replies the collection of plugin site URLs from where plugin lists can be downloaded.
1506 * @return the collection of plugin site URLs
1507 * @see #getOnlinePluginSites
1508 */
1509 public Collection<String> getPluginSites() {
1510 return getCollection("pluginmanager.sites", Collections.singleton(Main.getJOSMWebsite()+"/pluginicons%<?plugins=>"));
1511 }
1512
1513 /**
1514 * Returns the list of plugin sites available according to offline mode settings.
1515 * @return the list of available plugin sites
1516 * @since 8471
1517 */
1518 public Collection<String> getOnlinePluginSites() {
1519 Collection<String> pluginSites = new ArrayList<>(getPluginSites());
1520 for (Iterator<String> it = pluginSites.iterator(); it.hasNext();) {
1521 try {
1522 OnlineResource.JOSM_WEBSITE.checkOfflineAccess(it.next(), Main.getJOSMWebsite());
1523 } catch (OfflineAccessException ex) {
1524 Main.warn(ex, false);
1525 it.remove();
1526 }
1527 }
1528 return pluginSites;
1529 }
1530
1531 /**
1532 * Sets the collection of plugin site URLs.
1533 *
1534 * @param sites the site URLs
1535 */
1536 public void setPluginSites(Collection<String> sites) {
1537 putCollection("pluginmanager.sites", sites);
1538 }
1539
1540 protected XMLStreamReader parser;
1541
1542 public void validateXML(Reader in) throws IOException, SAXException {
1543 SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
1544 try (InputStream xsdStream = new CachedFile("resource://data/preferences.xsd").getInputStream()) {
1545 Schema schema = factory.newSchema(new StreamSource(xsdStream));
1546 Validator validator = schema.newValidator();
1547 validator.validate(new StreamSource(in));
1548 }
1549 }
1550
1551 public void fromXML(Reader in) throws XMLStreamException {
1552 XMLStreamReader parser = XMLInputFactory.newInstance().createXMLStreamReader(in);
1553 this.parser = parser;
1554 parse();
1555 }
1556
1557 public void parse() throws XMLStreamException {
1558 int event = parser.getEventType();
1559 while (true) {
1560 if (event == XMLStreamConstants.START_ELEMENT) {
1561 parseRoot();
1562 } else if (event == XMLStreamConstants.END_ELEMENT) {
1563 return;
1564 }
1565 if (parser.hasNext()) {
1566 event = parser.next();
1567 } else {
1568 break;
1569 }
1570 }
1571 parser.close();
1572 }
1573
1574 public void parseRoot() throws XMLStreamException {
1575 while (true) {
1576 int event = parser.next();
1577 if (event == XMLStreamConstants.START_ELEMENT) {
1578 String localName = parser.getLocalName();
1579 switch(localName) {
1580 case "tag":
1581 settingsMap.put(parser.getAttributeValue(null, "key"), new StringSetting(parser.getAttributeValue(null, "value")));
1582 jumpToEnd();
1583 break;
1584 case "list":
1585 case "collection":
1586 case "lists":
1587 case "maps":
1588 parseToplevelList();
1589 break;
1590 default:
1591 throwException("Unexpected element: "+localName);
1592 }
1593 } else if (event == XMLStreamConstants.END_ELEMENT) {
1594 return;
1595 }
1596 }
1597 }
1598
1599 private void jumpToEnd() throws XMLStreamException {
1600 while (true) {
1601 int event = parser.next();
1602 if (event == XMLStreamConstants.START_ELEMENT) {
1603 jumpToEnd();
1604 } else if (event == XMLStreamConstants.END_ELEMENT) {
1605 return;
1606 }
1607 }
1608 }
1609
1610 protected void parseToplevelList() throws XMLStreamException {
1611 String key = parser.getAttributeValue(null, "key");
1612 String name = parser.getLocalName();
1613
1614 List<String> entries = null;
1615 List<List<String>> lists = null;
1616 List<Map<String, String>> maps = null;
1617 while (true) {
1618 int event = parser.next();
1619 if (event == XMLStreamConstants.START_ELEMENT) {
1620 String localName = parser.getLocalName();
1621 switch(localName) {
1622 case "entry":
1623 if (entries == null) {
1624 entries = new ArrayList<>();
1625 }
1626 entries.add(parser.getAttributeValue(null, "value"));
1627 jumpToEnd();
1628 break;
1629 case "list":
1630 if (lists == null) {
1631 lists = new ArrayList<>();
1632 }
1633 lists.add(parseInnerList());
1634 break;
1635 case "map":
1636 if (maps == null) {
1637 maps = new ArrayList<>();
1638 }
1639 maps.add(parseMap());
1640 break;
1641 default:
1642 throwException("Unexpected element: "+localName);
1643 }
1644 } else if (event == XMLStreamConstants.END_ELEMENT) {
1645 break;
1646 }
1647 }
1648 if (entries != null) {
1649 settingsMap.put(key, new ListSetting(Collections.unmodifiableList(entries)));
1650 } else if (lists != null) {
1651 settingsMap.put(key, new ListListSetting(Collections.unmodifiableList(lists)));
1652 } else if (maps != null) {
1653 settingsMap.put(key, new MapListSetting(Collections.unmodifiableList(maps)));
1654 } else {
1655 if ("lists".equals(name)) {
1656 settingsMap.put(key, new ListListSetting(Collections.<List<String>>emptyList()));
1657 } else if ("maps".equals(name)) {
1658 settingsMap.put(key, new MapListSetting(Collections.<Map<String, String>>emptyList()));
1659 } else {
1660 settingsMap.put(key, new ListSetting(Collections.<String>emptyList()));
1661 }
1662 }
1663 }
1664
1665 protected List<String> parseInnerList() throws XMLStreamException {
1666 List<String> entries = new ArrayList<>();
1667 while (true) {
1668 int event = parser.next();
1669 if (event == XMLStreamConstants.START_ELEMENT) {
1670 if ("entry".equals(parser.getLocalName())) {
1671 entries.add(parser.getAttributeValue(null, "value"));
1672 jumpToEnd();
1673 } else {
1674 throwException("Unexpected element: "+parser.getLocalName());
1675 }
1676 } else if (event == XMLStreamConstants.END_ELEMENT) {
1677 break;
1678 }
1679 }
1680 return Collections.unmodifiableList(entries);
1681 }
1682
1683 protected Map<String, String> parseMap() throws XMLStreamException {
1684 Map<String, String> map = new LinkedHashMap<>();
1685 while (true) {
1686 int event = parser.next();
1687 if (event == XMLStreamConstants.START_ELEMENT) {
1688 if ("tag".equals(parser.getLocalName())) {
1689 map.put(parser.getAttributeValue(null, "key"), parser.getAttributeValue(null, "value"));
1690 jumpToEnd();
1691 } else {
1692 throwException("Unexpected element: "+parser.getLocalName());
1693 }
1694 } else if (event == XMLStreamConstants.END_ELEMENT) {
1695 break;
1696 }
1697 }
1698 return Collections.unmodifiableMap(map);
1699 }
1700
1701 protected void throwException(String msg) {
1702 throw new RuntimeException(msg + tr(" (at line {0}, column {1})",
1703 parser.getLocation().getLineNumber(), parser.getLocation().getColumnNumber()));
1704 }
1705
1706 private class SettingToXml implements SettingVisitor {
1707 private StringBuilder b;
1708 private boolean noPassword;
1709 private String key;
1710
1711 public SettingToXml(StringBuilder b, boolean noPassword) {
1712 this.b = b;
1713 this.noPassword = noPassword;
1714 }
1715
1716 public void setKey(String key) {
1717 this.key = key;
1718 }
1719
1720 @Override
1721 public void visit(StringSetting setting) {
1722 if (noPassword && "osm-server.password".equals(key))
1723 return; // do not store plain password.
1724 /* don't save default values */
1725 if (setting.equals(defaultsMap.get(key)))
1726 return;
1727 b.append(" <tag key='");
1728 b.append(XmlWriter.encode(key));
1729 b.append("' value='");
1730 b.append(XmlWriter.encode(setting.getValue()));
1731 b.append("'/>\n");
1732 }
1733
1734 @Override
1735 public void visit(ListSetting setting) {
1736 /* don't save default values */
1737 if (setting.equals(defaultsMap.get(key)))
1738 return;
1739 b.append(" <list key='").append(XmlWriter.encode(key)).append("'>\n");
1740 for (String s : setting.getValue()) {
1741 b.append(" <entry value='").append(XmlWriter.encode(s)).append("'/>\n");
1742 }
1743 b.append(" </list>\n");
1744 }
1745
1746 @Override
1747 public void visit(ListListSetting setting) {
1748 /* don't save default values */
1749 if (setting.equals(defaultsMap.get(key)))
1750 return;
1751 b.append(" <lists key='").append(XmlWriter.encode(key)).append("'>\n");
1752 for (List<String> list : setting.getValue()) {
1753 b.append(" <list>\n");
1754 for (String s : list) {
1755 b.append(" <entry value='").append(XmlWriter.encode(s)).append("'/>\n");
1756 }
1757 b.append(" </list>\n");
1758 }
1759 b.append(" </lists>\n");
1760 }
1761
1762 @Override
1763 public void visit(MapListSetting setting) {
1764 b.append(" <maps key='").append(XmlWriter.encode(key)).append("'>\n");
1765 for (Map<String, String> struct : setting.getValue()) {
1766 b.append(" <map>\n");
1767 for (Entry<String, String> e : struct.entrySet()) {
1768 b.append(" <tag key='").append(XmlWriter.encode(e.getKey()))
1769 .append("' value='").append(XmlWriter.encode(e.getValue())).append("'/>\n");
1770 }
1771 b.append(" </map>\n");
1772 }
1773 b.append(" </maps>\n");
1774 }
1775 }
1776
1777 public String toXML(boolean nopass) {
1778 StringBuilder b = new StringBuilder(
1779 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<preferences xmlns=\"")
1780 .append(Main.getXMLBase()).append("/preferences-1.0\" version=\"")
1781 .append(Version.getInstance().getVersion()).append("\">\n");
1782 SettingToXml toXml = new SettingToXml(b, nopass);
1783 for (Entry<String, Setting<?>> e : settingsMap.entrySet()) {
1784 toXml.setKey(e.getKey());
1785 e.getValue().visit(toXml);
1786 }
1787 b.append("</preferences>\n");
1788 return b.toString();
1789 }
1790
1791 /**
1792 * Removes obsolete preference settings. If you throw out a once-used preference
1793 * setting, add it to the list here with an expiry date (written as comment). If you
1794 * see something with an expiry date in the past, remove it from the list.
1795 */
1796 public void removeObsolete() {
1797 // drop this block end of 2015
1798 // update old style JOSM server links to use zip now, see #10581
1799 // actually also cache and mirror entries should be cleared
1800 if (getInteger("josm.version", Version.getInstance().getVersion()) < 8099) {
1801 for (String key: new String[]{"mappaint.style.entries", "taggingpreset.entries"}) {
1802 Collection<Map<String, String>> data = getListOfStructs(key, (Collection<Map<String, String>>) null);
1803 if (data != null) {
1804 List<Map<String, String>> newlist = new ArrayList<Map<String, String>>();
1805 boolean modified = false;
1806 for (Map<String, String> map : data) {
1807 Map<String, String> newmap = new LinkedHashMap<String, String>();
1808 for (Entry<String, String> entry : map.entrySet()) {
1809 String val = entry.getValue();
1810 String mkey = entry.getKey();
1811 if ("url".equals(mkey) && val.contains("josm.openstreetmap.de/josmfile") && !val.contains("zip=1")) {
1812 val += "&zip=1";
1813 modified = true;
1814
1815 }
1816 newmap.put(mkey, val);
1817 }
1818 newlist.add(newmap);
1819 }
1820 if (modified) {
1821 putListOfStructs(key, newlist);
1822 }
1823 }
1824 }
1825 }
1826
1827 String[] obsolete = {
1828 "remote.control.host", // replaced by individual values for IPv4 and IPv6. To remove end of 2015
1829 "osm.notes.enableDownload", // was used prior to r8071 when notes was an hidden feature. To remove end of 2015
1830 "mappaint.style.migration.switchedToMapCSS", // was used prior to 8315 for MapCSS switch. To remove end of 2015
1831 "mappaint.style.migration.changedXmlName" // was used prior to 8315 for MapCSS switch. To remove end of 2015
1832 };
1833 for (String key : obsolete) {
1834 if (settingsMap.containsKey(key)) {
1835 settingsMap.remove(key);
1836 Main.info(tr("Preference setting {0} has been removed since it is no longer used.", key));
1837 }
1838 }
1839 }
1840
1841 /**
1842 * Enables or not the preferences file auto-save mechanism (save each time a setting is changed).
1843 * This behaviour is enabled by default.
1844 * @param enable if {@code true}, makes JOSM save preferences file each time a setting is changed
1845 * @since 7085
1846 */
1847 public final void enableSaveOnPut(boolean enable) {
1848 synchronized (this) {
1849 saveOnPut = enable;
1850 }
1851 }
1852}
Note: See TracBrowser for help on using the repository browser.