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

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

checkstyle

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