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

Last change on this file since 9558 was 9371, checked in by simon04, 8 years ago

Java 7: use Objects.equals and Objects.hash

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