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

Last change on this file since 9171 was 9129, checked in by bastiK, 8 years ago

javadoc

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