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

Last change on this file since 9621 was 9618, checked in by stoecker, 8 years ago

fix Jenkins warning, coding style fixes

  • Property svn:eol-style set to native
File size: 73.2 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
665 * by this method.
666 *
667 * @return the cache directory
668 */
669 public File getCacheDirectory() {
670 if (cacheDir != null)
671 return cacheDir;
672 String path = System.getProperty("josm.cache");
673 if (path != null) {
674 cacheDir = new File(path).getAbsoluteFile();
675 } else {
676 path = System.getProperty("josm.home");
677 if (path != null) {
678 cacheDir = new File(path, "cache");
679 } else {
680 path = get("cache.folder", null);
681 if (path != null) {
682 cacheDir = new File(path).getAbsoluteFile();
683 } else {
684 cacheDir = Main.platform.getDefaultCacheDirectory();
685 }
686 }
687 }
688 if (!cacheDir.exists() && !cacheDir.mkdirs()) {
689 Main.warn(tr("Failed to create missing cache directory: {0}", cacheDir.getAbsoluteFile()));
690 JOptionPane.showMessageDialog(
691 Main.parent,
692 tr("<html>Failed to create missing cache directory: {0}</html>", cacheDir.getAbsoluteFile()),
693 tr("Error"),
694 JOptionPane.ERROR_MESSAGE
695 );
696 }
697 return cacheDir;
698 }
699
700 private static void addPossibleResourceDir(Set<String> locations, String s) {
701 if (s != null) {
702 if (!s.endsWith(File.separator)) {
703 s += File.separator;
704 }
705 locations.add(s);
706 }
707 }
708
709 /**
710 * Returns a set of all existing directories where resources could be stored.
711 * @return A set of all existing directories where resources could be stored.
712 */
713 public Collection<String> getAllPossiblePreferenceDirs() {
714 Set<String> locations = new HashSet<>();
715 addPossibleResourceDir(locations, getPreferencesDirectory().getPath());
716 addPossibleResourceDir(locations, getUserDataDirectory().getPath());
717 addPossibleResourceDir(locations, System.getenv("JOSM_RESOURCES"));
718 addPossibleResourceDir(locations, System.getProperty("josm.resources"));
719 if (Main.isPlatformWindows()) {
720 String appdata = System.getenv("APPDATA");
721 if (System.getenv("ALLUSERSPROFILE") != null && appdata != null
722 && appdata.lastIndexOf(File.separator) != -1) {
723 appdata = appdata.substring(appdata.lastIndexOf(File.separator));
724 locations.add(new File(new File(System.getenv("ALLUSERSPROFILE"),
725 appdata), "JOSM").getPath());
726 }
727 } else {
728 locations.add("/usr/local/share/josm/");
729 locations.add("/usr/local/lib/josm/");
730 locations.add("/usr/share/josm/");
731 locations.add("/usr/lib/josm/");
732 }
733 return locations;
734 }
735
736 /**
737 * Get settings value for a certain key.
738 * @param key the identifier for the setting
739 * @return "" if there is nothing set for the preference key,
740 * the corresponding value otherwise. The result is not null.
741 */
742 public synchronized String get(final String key) {
743 String value = get(key, null);
744 return value == null ? "" : value;
745 }
746
747 /**
748 * Get settings value for a certain key and provide default a value.
749 * @param key the identifier for the setting
750 * @param def the default value. For each call of get() with a given key, the
751 * default value must be the same.
752 * @return the corresponding value if the property has been set before,
753 * def otherwise
754 */
755 public synchronized String get(final String key, final String def) {
756 return getSetting(key, new StringSetting(def), StringSetting.class).getValue();
757 }
758
759 public synchronized Map<String, String> getAllPrefix(final String prefix) {
760 final Map<String, String> all = new TreeMap<>();
761 for (final Entry<String, Setting<?>> e : settingsMap.entrySet()) {
762 if (e.getKey().startsWith(prefix) && (e.getValue() instanceof StringSetting)) {
763 all.put(e.getKey(), ((StringSetting) e.getValue()).getValue());
764 }
765 }
766 return all;
767 }
768
769 public synchronized List<String> getAllPrefixCollectionKeys(final String prefix) {
770 final List<String> all = new LinkedList<>();
771 for (Map.Entry<String, Setting<?>> entry : settingsMap.entrySet()) {
772 if (entry.getKey().startsWith(prefix) && entry.getValue() instanceof ListSetting) {
773 all.add(entry.getKey());
774 }
775 }
776 return all;
777 }
778
779 public synchronized Map<String, String> getAllColors() {
780 final Map<String, String> all = new TreeMap<>();
781 for (final Entry<String, Setting<?>> e : defaultsMap.entrySet()) {
782 if (e.getKey().startsWith("color.") && e.getValue() instanceof StringSetting) {
783 StringSetting d = (StringSetting) e.getValue();
784 if (d.getValue() != null) {
785 all.put(e.getKey().substring(6), d.getValue());
786 }
787 }
788 }
789 for (final Entry<String, Setting<?>> e : settingsMap.entrySet()) {
790 if (e.getKey().startsWith("color.") && (e.getValue() instanceof StringSetting)) {
791 all.put(e.getKey().substring(6), ((StringSetting) e.getValue()).getValue());
792 }
793 }
794 return all;
795 }
796
797 public synchronized boolean getBoolean(final String key) {
798 String s = get(key, null);
799 return s != null && Boolean.parseBoolean(s);
800 }
801
802 public synchronized boolean getBoolean(final String key, final boolean def) {
803 return Boolean.parseBoolean(get(key, Boolean.toString(def)));
804 }
805
806 public synchronized boolean getBoolean(final String key, final String specName, final boolean def) {
807 boolean generic = getBoolean(key, def);
808 String skey = key+'.'+specName;
809 Setting<?> prop = settingsMap.get(skey);
810 if (prop instanceof StringSetting)
811 return Boolean.parseBoolean(((StringSetting) prop).getValue());
812 else
813 return generic;
814 }
815
816 /**
817 * Set a value for a certain setting.
818 * @param key the unique identifier for the setting
819 * @param value the value of the setting. Can be null or "" which both removes
820 * the key-value entry.
821 * @return {@code true}, if something has changed (i.e. value is different than before)
822 */
823 public boolean put(final String key, String value) {
824 if (value != null && value.isEmpty()) {
825 value = null;
826 }
827 return putSetting(key, value == null ? null : new StringSetting(value));
828 }
829
830 public boolean put(final String key, final boolean value) {
831 return put(key, Boolean.toString(value));
832 }
833
834 public boolean putInteger(final String key, final Integer value) {
835 return put(key, Integer.toString(value));
836 }
837
838 public boolean putDouble(final String key, final Double value) {
839 return put(key, Double.toString(value));
840 }
841
842 public boolean putLong(final String key, final Long value) {
843 return put(key, Long.toString(value));
844 }
845
846 /**
847 * Called after every put. In case of a problem, do nothing but output the error in log.
848 * @throws IOException if any I/O error occurs
849 */
850 public void save() throws IOException {
851 /* currently unused, but may help to fix configuration issues in future */
852 putInteger("josm.version", Version.getInstance().getVersion());
853
854 updateSystemProperties();
855
856 File prefFile = getPreferenceFile();
857 File backupFile = new File(prefFile + "_backup");
858
859 // Backup old preferences if there are old preferences
860 if (prefFile.exists() && prefFile.length() > 0 && initSuccessful) {
861 Utils.copyFile(prefFile, backupFile);
862 }
863
864 try (PrintWriter out = new PrintWriter(new OutputStreamWriter(
865 new FileOutputStream(prefFile + "_tmp"), StandardCharsets.UTF_8), false)) {
866 out.print(toXML(false));
867 }
868
869 File tmpFile = new File(prefFile + "_tmp");
870 Utils.copyFile(tmpFile, prefFile);
871 Utils.deleteFile(tmpFile, marktr("Unable to delete temporary file {0}"));
872
873 setCorrectPermissions(prefFile);
874 setCorrectPermissions(backupFile);
875 }
876
877 private static void setCorrectPermissions(File file) {
878 if (!file.setReadable(false, false) && Main.isDebugEnabled()) {
879 Main.debug(tr("Unable to set file non-readable {0}", file.getAbsolutePath()));
880 }
881 if (!file.setWritable(false, false) && Main.isDebugEnabled()) {
882 Main.debug(tr("Unable to set file non-writable {0}", file.getAbsolutePath()));
883 }
884 if (!file.setExecutable(false, false) && Main.isDebugEnabled()) {
885 Main.debug(tr("Unable to set file non-executable {0}", file.getAbsolutePath()));
886 }
887 if (!file.setReadable(true, true) && Main.isDebugEnabled()) {
888 Main.debug(tr("Unable to set file readable {0}", file.getAbsolutePath()));
889 }
890 if (!file.setWritable(true, true) && Main.isDebugEnabled()) {
891 Main.debug(tr("Unable to set file writable {0}", file.getAbsolutePath()));
892 }
893 }
894
895 /**
896 * Loads preferences from settings file.
897 * @throws IOException if any I/O error occurs while reading the file
898 * @throws SAXException if the settings file does not contain valid XML
899 * @throws XMLStreamException if an XML error occurs while parsing the file (after validation)
900 */
901 protected void load() throws IOException, SAXException, XMLStreamException {
902 settingsMap.clear();
903 File pref = getPreferenceFile();
904 try (BufferedReader in = Files.newBufferedReader(pref.toPath(), StandardCharsets.UTF_8)) {
905 validateXML(in);
906 }
907 try (BufferedReader in = Files.newBufferedReader(pref.toPath(), StandardCharsets.UTF_8)) {
908 fromXML(in);
909 }
910 updateSystemProperties();
911 removeObsolete();
912 }
913
914 /**
915 * Initializes preferences.
916 * @param reset if {@code true}, current settings file is replaced by the default one
917 */
918 public void init(boolean reset) {
919 initSuccessful = false;
920 // get the preferences.
921 File prefDir = getPreferencesDirectory();
922 if (prefDir.exists()) {
923 if (!prefDir.isDirectory()) {
924 Main.warn(tr("Failed to initialize preferences. Preference directory ''{0}'' is not a directory.",
925 prefDir.getAbsoluteFile()));
926 JOptionPane.showMessageDialog(
927 Main.parent,
928 tr("<html>Failed to initialize preferences.<br>Preference directory ''{0}'' is not a directory.</html>",
929 prefDir.getAbsoluteFile()),
930 tr("Error"),
931 JOptionPane.ERROR_MESSAGE
932 );
933 return;
934 }
935 } else {
936 if (!prefDir.mkdirs()) {
937 Main.warn(tr("Failed to initialize preferences. Failed to create missing preference directory: {0}",
938 prefDir.getAbsoluteFile()));
939 JOptionPane.showMessageDialog(
940 Main.parent,
941 tr("<html>Failed to initialize preferences.<br>Failed to create missing preference directory: {0}</html>",
942 prefDir.getAbsoluteFile()),
943 tr("Error"),
944 JOptionPane.ERROR_MESSAGE
945 );
946 return;
947 }
948 }
949
950 File preferenceFile = getPreferenceFile();
951 try {
952 if (!preferenceFile.exists()) {
953 Main.info(tr("Missing preference file ''{0}''. Creating a default preference file.", preferenceFile.getAbsoluteFile()));
954 resetToDefault();
955 save();
956 } else if (reset) {
957 File backupFile = new File(prefDir, "preferences.xml.bak");
958 Main.platform.rename(preferenceFile, backupFile);
959 Main.warn(tr("Replacing existing preference file ''{0}'' with default preference file.", preferenceFile.getAbsoluteFile()));
960 resetToDefault();
961 save();
962 }
963 } catch (IOException e) {
964 Main.error(e);
965 JOptionPane.showMessageDialog(
966 Main.parent,
967 tr("<html>Failed to initialize preferences.<br>Failed to reset preference file to default: {0}</html>",
968 getPreferenceFile().getAbsoluteFile()),
969 tr("Error"),
970 JOptionPane.ERROR_MESSAGE
971 );
972 return;
973 }
974 try {
975 load();
976 initSuccessful = true;
977 } catch (Exception e) {
978 Main.error(e);
979 File backupFile = new File(prefDir, "preferences.xml.bak");
980 JOptionPane.showMessageDialog(
981 Main.parent,
982 tr("<html>Preferences file had errors.<br> Making backup of old one to <br>{0}<br> " +
983 "and creating a new default preference file.</html>",
984 backupFile.getAbsoluteFile()),
985 tr("Error"),
986 JOptionPane.ERROR_MESSAGE
987 );
988 Main.platform.rename(preferenceFile, backupFile);
989 try {
990 resetToDefault();
991 save();
992 } catch (IOException e1) {
993 Main.error(e1);
994 Main.warn(tr("Failed to initialize preferences. Failed to reset preference file to default: {0}", getPreferenceFile()));
995 }
996 }
997 }
998
999 public final void resetToDefault() {
1000 settingsMap.clear();
1001 }
1002
1003 /**
1004 * Convenience method for accessing colour preferences.
1005 *
1006 * @param colName name of the colour
1007 * @param def default value
1008 * @return a Color object for the configured colour, or the default value if none configured.
1009 */
1010 public synchronized Color getColor(String colName, Color def) {
1011 return getColor(colName, null, def);
1012 }
1013
1014 /* only for preferences */
1015 public synchronized String getColorName(String o) {
1016 try {
1017 Matcher m = Pattern.compile("mappaint\\.(.+?)\\.(.+)").matcher(o);
1018 if (m.matches()) {
1019 return tr("Paint style {0}: {1}", tr(I18n.escape(m.group(1))), tr(I18n.escape(m.group(2))));
1020 }
1021 } catch (Exception e) {
1022 Main.warn(e);
1023 }
1024 try {
1025 Matcher m = Pattern.compile("layer (.+)").matcher(o);
1026 if (m.matches()) {
1027 return tr("Layer: {0}", tr(I18n.escape(m.group(1))));
1028 }
1029 } catch (Exception e) {
1030 Main.warn(e);
1031 }
1032 return tr(I18n.escape(colornames.containsKey(o) ? colornames.get(o) : o));
1033 }
1034
1035 /**
1036 * Returns the color for the given key.
1037 * @param key The color key
1038 * @return the color
1039 */
1040 public Color getColor(ColorKey key) {
1041 return getColor(key.getColorName(), key.getSpecialName(), key.getDefaultValue());
1042 }
1043
1044 /**
1045 * Convenience method for accessing colour preferences.
1046 *
1047 * @param colName name of the colour
1048 * @param specName name of the special colour settings
1049 * @param def default value
1050 * @return a Color object for the configured colour, or the default value if none configured.
1051 */
1052 public synchronized Color getColor(String colName, String specName, Color def) {
1053 String colKey = ColorProperty.getColorKey(colName);
1054 if (!colKey.equals(colName)) {
1055 colornames.put(colKey, colName);
1056 }
1057 String colStr = specName != null ? get("color."+specName) : "";
1058 if (colStr.isEmpty()) {
1059 colStr = get("color." + colKey, ColorHelper.color2html(def, true));
1060 }
1061 if (colStr != null && !colStr.isEmpty()) {
1062 return ColorHelper.html2color(colStr);
1063 } else {
1064 return def;
1065 }
1066 }
1067
1068 public synchronized Color getDefaultColor(String colKey) {
1069 StringSetting col = Utils.cast(defaultsMap.get("color."+colKey), StringSetting.class);
1070 String colStr = col == null ? null : col.getValue();
1071 return colStr == null || colStr.isEmpty() ? null : ColorHelper.html2color(colStr);
1072 }
1073
1074 public synchronized boolean putColor(String colKey, Color val) {
1075 return put("color."+colKey, val != null ? ColorHelper.color2html(val, true) : null);
1076 }
1077
1078 public synchronized int getInteger(String key, int def) {
1079 String v = get(key, Integer.toString(def));
1080 if (v.isEmpty())
1081 return def;
1082
1083 try {
1084 return Integer.parseInt(v);
1085 } catch (NumberFormatException e) {
1086 // fall out
1087 if (Main.isTraceEnabled()) {
1088 Main.trace(e.getMessage());
1089 }
1090 }
1091 return def;
1092 }
1093
1094 public synchronized int getInteger(String key, String specName, int def) {
1095 String v = get(key+'.'+specName);
1096 if (v.isEmpty())
1097 v = get(key, Integer.toString(def));
1098 if (v.isEmpty())
1099 return def;
1100
1101 try {
1102 return Integer.parseInt(v);
1103 } catch (NumberFormatException e) {
1104 // fall out
1105 if (Main.isTraceEnabled()) {
1106 Main.trace(e.getMessage());
1107 }
1108 }
1109 return def;
1110 }
1111
1112 public synchronized long getLong(String key, long def) {
1113 String v = get(key, Long.toString(def));
1114 if (null == v)
1115 return def;
1116
1117 try {
1118 return Long.parseLong(v);
1119 } catch (NumberFormatException e) {
1120 // fall out
1121 if (Main.isTraceEnabled()) {
1122 Main.trace(e.getMessage());
1123 }
1124 }
1125 return def;
1126 }
1127
1128 public synchronized double getDouble(String key, double def) {
1129 String v = get(key, Double.toString(def));
1130 if (null == v)
1131 return def;
1132
1133 try {
1134 return Double.parseDouble(v);
1135 } catch (NumberFormatException e) {
1136 // fall out
1137 if (Main.isTraceEnabled()) {
1138 Main.trace(e.getMessage());
1139 }
1140 }
1141 return def;
1142 }
1143
1144 /**
1145 * Get a list of values for a certain key
1146 * @param key the identifier for the setting
1147 * @param def the default value.
1148 * @return the corresponding value if the property has been set before,
1149 * def otherwise
1150 */
1151 public Collection<String> getCollection(String key, Collection<String> def) {
1152 return getSetting(key, ListSetting.create(def), ListSetting.class).getValue();
1153 }
1154
1155 /**
1156 * Get a list of values for a certain key
1157 * @param key the identifier for the setting
1158 * @return the corresponding value if the property has been set before,
1159 * an empty Collection otherwise.
1160 */
1161 public Collection<String> getCollection(String key) {
1162 Collection<String> val = getCollection(key, null);
1163 return val == null ? Collections.<String>emptyList() : val;
1164 }
1165
1166 public synchronized void removeFromCollection(String key, String value) {
1167 List<String> a = new ArrayList<>(getCollection(key, Collections.<String>emptyList()));
1168 a.remove(value);
1169 putCollection(key, a);
1170 }
1171
1172 /**
1173 * Set a value for a certain setting. The changed setting is saved
1174 * to the preference file immediately. Due to caching mechanisms on modern
1175 * operating systems and hardware, this shouldn't be a performance problem.
1176 * @param key the unique identifier for the setting
1177 * @param setting the value of the setting. In case it is null, the key-value
1178 * entry will be removed.
1179 * @return {@code true}, if something has changed (i.e. value is different than before)
1180 */
1181 public boolean putSetting(final String key, Setting<?> setting) {
1182 CheckParameterUtil.ensureParameterNotNull(key);
1183 if (setting != null && setting.getValue() == null)
1184 throw new IllegalArgumentException("setting argument must not have null value");
1185 Setting<?> settingOld;
1186 Setting<?> settingCopy = null;
1187 synchronized (this) {
1188 if (setting == null) {
1189 settingOld = settingsMap.remove(key);
1190 if (settingOld == null)
1191 return false;
1192 } else {
1193 settingOld = settingsMap.get(key);
1194 if (setting.equals(settingOld))
1195 return false;
1196 if (settingOld == null && setting.equals(defaultsMap.get(key)))
1197 return false;
1198 settingCopy = setting.copy();
1199 settingsMap.put(key, settingCopy);
1200 }
1201 if (saveOnPut) {
1202 try {
1203 save();
1204 } catch (IOException e) {
1205 Main.warn(tr("Failed to persist preferences to ''{0}''", getPreferenceFile().getAbsoluteFile()));
1206 }
1207 }
1208 }
1209 // Call outside of synchronized section in case some listener wait for other thread that wait for preference lock
1210 firePreferenceChanged(key, settingOld, settingCopy);
1211 return true;
1212 }
1213
1214 public synchronized Setting<?> getSetting(String key, Setting<?> def) {
1215 return getSetting(key, def, Setting.class);
1216 }
1217
1218 /**
1219 * Get settings value for a certain key and provide default a value.
1220 * @param <T> the setting type
1221 * @param key the identifier for the setting
1222 * @param def the default value. For each call of getSetting() with a given
1223 * key, the default value must be the same. <code>def</code> must not be
1224 * null, but the value of <code>def</code> can be null.
1225 * @param klass the setting type (same as T)
1226 * @return the corresponding value if the property has been set before,
1227 * def otherwise
1228 */
1229 @SuppressWarnings("unchecked")
1230 public synchronized <T extends Setting<?>> T getSetting(String key, T def, Class<T> klass) {
1231 CheckParameterUtil.ensureParameterNotNull(key);
1232 CheckParameterUtil.ensureParameterNotNull(def);
1233 Setting<?> oldDef = defaultsMap.get(key);
1234 if (oldDef != null && oldDef.getValue() != null && def.getValue() != null && !def.equals(oldDef)) {
1235 Main.info("Defaults for " + key + " differ: " + def + " != " + defaultsMap.get(key));
1236 }
1237 if (def.getValue() != null || oldDef == null) {
1238 defaultsMap.put(key, def.copy());
1239 }
1240 Setting<?> prop = settingsMap.get(key);
1241 if (klass.isInstance(prop)) {
1242 return (T) prop;
1243 } else {
1244 return def;
1245 }
1246 }
1247
1248 /**
1249 * Put a collection.
1250 * @param key key
1251 * @param value value
1252 * @return {@code true}, if something has changed (i.e. value is different than before)
1253 */
1254 public boolean putCollection(String key, Collection<String> value) {
1255 return putSetting(key, value == null ? null : ListSetting.create(value));
1256 }
1257
1258 /**
1259 * Saves at most {@code maxsize} items of collection {@code val}.
1260 * @param key key
1261 * @param maxsize max number of items to save
1262 * @param val value
1263 * @return {@code true}, if something has changed (i.e. value is different than before)
1264 */
1265 public boolean putCollectionBounded(String key, int maxsize, Collection<String> val) {
1266 Collection<String> newCollection = new ArrayList<>(Math.min(maxsize, val.size()));
1267 for (String i : val) {
1268 if (newCollection.size() >= maxsize) {
1269 break;
1270 }
1271 newCollection.add(i);
1272 }
1273 return putCollection(key, newCollection);
1274 }
1275
1276 /**
1277 * Used to read a 2-dimensional array of strings from the preference file.
1278 * If not a single entry could be found, <code>def</code> is returned.
1279 * @param key preference key
1280 * @param def default array value
1281 * @return array value
1282 */
1283 @SuppressWarnings({ "unchecked", "rawtypes" })
1284 public synchronized Collection<Collection<String>> getArray(String key, Collection<Collection<String>> def) {
1285 ListListSetting val = getSetting(key, ListListSetting.create(def), ListListSetting.class);
1286 return (Collection) val.getValue();
1287 }
1288
1289 public Collection<Collection<String>> getArray(String key) {
1290 Collection<Collection<String>> res = getArray(key, null);
1291 return res == null ? Collections.<Collection<String>>emptyList() : res;
1292 }
1293
1294 /**
1295 * Put an array.
1296 * @param key key
1297 * @param value value
1298 * @return {@code true}, if something has changed (i.e. value is different than before)
1299 */
1300 public boolean putArray(String key, Collection<Collection<String>> value) {
1301 return putSetting(key, value == null ? null : ListListSetting.create(value));
1302 }
1303
1304 public Collection<Map<String, String>> getListOfStructs(String key, Collection<Map<String, String>> def) {
1305 return getSetting(key, new MapListSetting(def == null ? null : new ArrayList<>(def)), MapListSetting.class).getValue();
1306 }
1307
1308 public boolean putListOfStructs(String key, Collection<Map<String, String>> value) {
1309 return putSetting(key, value == null ? null : new MapListSetting(new ArrayList<>(value)));
1310 }
1311
1312 /**
1313 * Annotation used for converting objects to String Maps and vice versa.
1314 * Indicates that a certain field should be considered in the conversion
1315 * process. Otherwise it is ignored.
1316 *
1317 * @see #serializeStruct(java.lang.Object, java.lang.Class)
1318 * @see #deserializeStruct(java.util.Map, java.lang.Class)
1319 */
1320 @Retention(RetentionPolicy.RUNTIME) // keep annotation at runtime
1321 public @interface pref { }
1322
1323 /**
1324 * Annotation used for converting objects to String Maps.
1325 * Indicates that a certain field should be written to the map, even if
1326 * the value is the same as the default value.
1327 *
1328 * @see #serializeStruct(java.lang.Object, java.lang.Class)
1329 */
1330 @Retention(RetentionPolicy.RUNTIME) // keep annotation at runtime
1331 public @interface writeExplicitly { }
1332
1333 /**
1334 * Get a list of hashes which are represented by a struct-like class.
1335 * Possible properties are given by fields of the class klass that have
1336 * the @pref annotation.
1337 * Default constructor is used to initialize the struct objects, properties
1338 * then override some of these default values.
1339 * @param <T> klass type
1340 * @param key main preference key
1341 * @param klass The struct class
1342 * @return a list of objects of type T or an empty list if nothing was found
1343 */
1344 public <T> List<T> getListOfStructs(String key, Class<T> klass) {
1345 List<T> r = getListOfStructs(key, null, klass);
1346 if (r == null)
1347 return Collections.emptyList();
1348 else
1349 return r;
1350 }
1351
1352 /**
1353 * same as above, but returns def if nothing was found
1354 * @param <T> klass type
1355 * @param key main preference key
1356 * @param def default value
1357 * @param klass The struct class
1358 * @return a list of objects of type T or {@code def} if nothing was found
1359 */
1360 public <T> List<T> getListOfStructs(String key, Collection<T> def, Class<T> klass) {
1361 Collection<Map<String, String>> prop =
1362 getListOfStructs(key, def == null ? null : serializeListOfStructs(def, klass));
1363 if (prop == null)
1364 return def == null ? null : new ArrayList<>(def);
1365 List<T> lst = new ArrayList<>();
1366 for (Map<String, String> entries : prop) {
1367 T struct = deserializeStruct(entries, klass);
1368 lst.add(struct);
1369 }
1370 return lst;
1371 }
1372
1373 /**
1374 * Convenience method that saves a MapListSetting which is provided as a
1375 * Collection of objects.
1376 *
1377 * Each object is converted to a <code>Map&lt;String, String&gt;</code> using
1378 * the fields with {@link pref} annotation. The field name is the key and
1379 * the value will be converted to a string.
1380 *
1381 * Considers only fields that have the @pref annotation.
1382 * In addition it does not write fields with null values. (Thus they are cleared)
1383 * Default values are given by the field values after default constructor has
1384 * been called.
1385 * Fields equal to the default value are not written unless the field has
1386 * the @writeExplicitly annotation.
1387 * @param <T> the class,
1388 * @param key main preference key
1389 * @param val the list that is supposed to be saved
1390 * @param klass The struct class
1391 * @return true if something has changed
1392 */
1393 public <T> boolean putListOfStructs(String key, Collection<T> val, Class<T> klass) {
1394 return putListOfStructs(key, serializeListOfStructs(val, klass));
1395 }
1396
1397 private static <T> Collection<Map<String, String>> serializeListOfStructs(Collection<T> l, Class<T> klass) {
1398 if (l == null)
1399 return null;
1400 Collection<Map<String, String>> vals = new ArrayList<>();
1401 for (T struct : l) {
1402 if (struct == null) {
1403 continue;
1404 }
1405 vals.add(serializeStruct(struct, klass));
1406 }
1407 return vals;
1408 }
1409
1410 @SuppressWarnings("rawtypes")
1411 private static String mapToJson(Map map) {
1412 StringWriter stringWriter = new StringWriter();
1413 try (JsonWriter writer = Json.createWriter(stringWriter)) {
1414 JsonObjectBuilder object = Json.createObjectBuilder();
1415 for (Object o: map.entrySet()) {
1416 Entry e = (Entry) o;
1417 Object evalue = e.getValue();
1418 if (evalue instanceof Collection) {
1419 JsonArrayBuilder a = Json.createArrayBuilder();
1420 for (Object evo: (Collection) evalue) {
1421 a.add(evo.toString());
1422 }
1423 object.add(e.getKey().toString(), a.build());
1424 } else {
1425 object.add(e.getKey().toString(), evalue.toString());
1426 }
1427 }
1428 writer.writeObject(object.build());
1429 }
1430 return stringWriter.toString();
1431 }
1432
1433 @SuppressWarnings({ "rawtypes", "unchecked" })
1434 private static Map mapFromJson(String s) {
1435 Map ret = null;
1436 try (JsonReader reader = Json.createReader(new StringReader(s))) {
1437 JsonObject object = reader.readObject();
1438 ret = new HashMap(object.size());
1439 for (Entry<String, JsonValue> e: object.entrySet()) {
1440 JsonValue value = e.getValue();
1441 if (value instanceof JsonArray) {
1442 List<String> finalList = new ArrayList<String>();
1443 for (JsonString js: ((JsonArray) value).getValuesAs(JsonString.class)) {
1444 finalList.add(js.getString());
1445 }
1446 ret.put(e.getKey(), finalList);
1447 } else if (value instanceof JsonString) {
1448 // in some cases, when JsonValue.toString() is called, then additional quotation marks are left in value
1449 ret.put(e.getKey(), ((JsonString) value).getString());
1450 } else {
1451 ret.put(e.getKey(), e.getValue().toString());
1452 }
1453 }
1454 }
1455 return ret;
1456 }
1457
1458 /**
1459 * Convert an object to a String Map, by using field names and values as map
1460 * key and value.
1461 *
1462 * The field value is converted to a String.
1463 *
1464 * Only fields with annotation {@link pref} are taken into account.
1465 *
1466 * Fields will not be written to the map if the value is null or unchanged
1467 * (compared to an object created with the no-arg-constructor).
1468 * The {@link writeExplicitly} annotation overrides this behavior, i.e. the
1469 * default value will also be written.
1470 *
1471 * @param <T> the class of the object <code>struct</code>
1472 * @param struct the object to be converted
1473 * @param klass the class T
1474 * @return the resulting map (same data content as <code>struct</code>)
1475 */
1476 public static <T> Map<String, String> serializeStruct(T struct, Class<T> klass) {
1477 T structPrototype;
1478 try {
1479 structPrototype = klass.newInstance();
1480 } catch (InstantiationException | IllegalAccessException ex) {
1481 throw new RuntimeException(ex);
1482 }
1483
1484 Map<String, String> hash = new LinkedHashMap<>();
1485 for (Field f : klass.getDeclaredFields()) {
1486 if (f.getAnnotation(pref.class) == null) {
1487 continue;
1488 }
1489 f.setAccessible(true);
1490 try {
1491 Object fieldValue = f.get(struct);
1492 Object defaultFieldValue = f.get(structPrototype);
1493 if (fieldValue != null) {
1494 if (f.getAnnotation(writeExplicitly.class) != null || !Objects.equals(fieldValue, defaultFieldValue)) {
1495 String key = f.getName().replace('_', '-');
1496 if (fieldValue instanceof Map) {
1497 hash.put(key, mapToJson((Map) fieldValue));
1498 } else {
1499 hash.put(key, fieldValue.toString());
1500 }
1501 }
1502 }
1503 } catch (IllegalArgumentException | IllegalAccessException ex) {
1504 throw new RuntimeException(ex);
1505 }
1506 }
1507 return hash;
1508 }
1509
1510 /**
1511 * Converts a String-Map to an object of a certain class, by comparing
1512 * map keys to field names of the class and assigning map values to the
1513 * corresponding fields.
1514 *
1515 * The map value (a String) is converted to the field type. Supported
1516 * types are: boolean, Boolean, int, Integer, double, Double, String,
1517 * Map&lt;String, String&gt; and Map&lt;String, List&lt;String&gt;&gt;.
1518 *
1519 * Only fields with annotation {@link pref} are taken into account.
1520 * @param <T> the class
1521 * @param hash the string map with initial values
1522 * @param klass the class T
1523 * @return an object of class T, initialized as described above
1524 */
1525 public static <T> T deserializeStruct(Map<String, String> hash, Class<T> klass) {
1526 T struct = null;
1527 try {
1528 struct = klass.newInstance();
1529 } catch (InstantiationException | IllegalAccessException ex) {
1530 throw new RuntimeException(ex);
1531 }
1532 for (Entry<String, String> key_value : hash.entrySet()) {
1533 Object value = null;
1534 Field f;
1535 try {
1536 f = klass.getDeclaredField(key_value.getKey().replace('-', '_'));
1537 } catch (NoSuchFieldException ex) {
1538 continue;
1539 } catch (SecurityException ex) {
1540 throw new RuntimeException(ex);
1541 }
1542 if (f.getAnnotation(pref.class) == null) {
1543 continue;
1544 }
1545 f.setAccessible(true);
1546 if (f.getType() == Boolean.class || f.getType() == boolean.class) {
1547 value = Boolean.valueOf(key_value.getValue());
1548 } else if (f.getType() == Integer.class || f.getType() == int.class) {
1549 try {
1550 value = Integer.valueOf(key_value.getValue());
1551 } catch (NumberFormatException nfe) {
1552 continue;
1553 }
1554 } else if (f.getType() == Double.class || f.getType() == double.class) {
1555 try {
1556 value = Double.valueOf(key_value.getValue());
1557 } catch (NumberFormatException nfe) {
1558 continue;
1559 }
1560 } else if (f.getType() == String.class) {
1561 value = key_value.getValue();
1562 } else if (f.getType().isAssignableFrom(Map.class)) {
1563 value = mapFromJson(key_value.getValue());
1564 } else
1565 throw new RuntimeException("unsupported preference primitive type");
1566
1567 try {
1568 f.set(struct, value);
1569 } catch (IllegalArgumentException ex) {
1570 throw new AssertionError(ex);
1571 } catch (IllegalAccessException ex) {
1572 throw new RuntimeException(ex);
1573 }
1574 }
1575 return struct;
1576 }
1577
1578 public Map<String, Setting<?>> getAllSettings() {
1579 return new TreeMap<>(settingsMap);
1580 }
1581
1582 public Map<String, Setting<?>> getAllDefaults() {
1583 return new TreeMap<>(defaultsMap);
1584 }
1585
1586 /**
1587 * Updates system properties with the current values in the preferences.
1588 *
1589 */
1590 public void updateSystemProperties() {
1591 if ("true".equals(get("prefer.ipv6", "auto"))) {
1592 // never set this to false, only true!
1593 if (!"true".equals(Utils.updateSystemProperty("java.net.preferIPv6Addresses", "true"))) {
1594 Main.info(tr("Try enabling IPv6 network, prefering IPv6 over IPv4 (only works on early startup)."));
1595 }
1596 }
1597 Utils.updateSystemProperty("http.agent", Version.getInstance().getAgentString());
1598 Utils.updateSystemProperty("user.language", get("language"));
1599 // Workaround to fix a Java bug.
1600 // Force AWT toolkit to update its internal preferences (fix #6345).
1601 // This ugly hack comes from Sun bug database: https://bugs.openjdk.java.net/browse/JDK-6292739
1602 if (!GraphicsEnvironment.isHeadless()) {
1603 try {
1604 Field field = Toolkit.class.getDeclaredField("resources");
1605 field.setAccessible(true);
1606 field.set(null, ResourceBundle.getBundle("sun.awt.resources.awt"));
1607 } catch (Exception | InternalError e) {
1608 // Ignore all exceptions, including internal error raised by Java 9 Jigsaw EA:
1609 // java.lang.InternalError: legacy getBundle can't be used to find sun.awt.resources.awt in module java.desktop
1610 // InternalError catch to remove when https://bugs.openjdk.java.net/browse/JDK-8136804 is resolved
1611 if (Main.isTraceEnabled()) {
1612 Main.trace(e.getMessage());
1613 }
1614 }
1615 }
1616 // Possibility to disable SNI (not by default) in case of misconfigured https servers
1617 // See #9875 + http://stackoverflow.com/a/14884941/2257172
1618 // then https://josm.openstreetmap.de/ticket/12152#comment:5 for details
1619 if (getBoolean("jdk.tls.disableSNIExtension", false)) {
1620 Utils.updateSystemProperty("jsse.enableSNIExtension", "false");
1621 }
1622 // Workaround to fix another Java bug
1623 // Force Java 7 to use old sorting algorithm of Arrays.sort (fix #8712).
1624 // See Oracle bug database: https://bugs.openjdk.java.net/browse/JDK-7075600
1625 // and https://bugs.openjdk.java.net/browse/JDK-6923200
1626 // The bug seems to have been fixed in Java 8, to remove during transition
1627 if (getBoolean("jdk.Arrays.useLegacyMergeSort", !Version.getInstance().isLocalBuild())) {
1628 Utils.updateSystemProperty("java.util.Arrays.useLegacyMergeSort", "true");
1629 }
1630 }
1631
1632 /**
1633 * Replies the collection of plugin site URLs from where plugin lists can be downloaded.
1634 * @return the collection of plugin site URLs
1635 * @see #getOnlinePluginSites
1636 */
1637 public Collection<String> getPluginSites() {
1638 return getCollection("pluginmanager.sites", Collections.singleton(Main.getJOSMWebsite()+"/pluginicons%<?plugins=>"));
1639 }
1640
1641 /**
1642 * Returns the list of plugin sites available according to offline mode settings.
1643 * @return the list of available plugin sites
1644 * @since 8471
1645 */
1646 public Collection<String> getOnlinePluginSites() {
1647 Collection<String> pluginSites = new ArrayList<>(getPluginSites());
1648 for (Iterator<String> it = pluginSites.iterator(); it.hasNext();) {
1649 try {
1650 OnlineResource.JOSM_WEBSITE.checkOfflineAccess(it.next(), Main.getJOSMWebsite());
1651 } catch (OfflineAccessException ex) {
1652 Main.warn(ex, false);
1653 it.remove();
1654 }
1655 }
1656 return pluginSites;
1657 }
1658
1659 /**
1660 * Sets the collection of plugin site URLs.
1661 *
1662 * @param sites the site URLs
1663 */
1664 public void setPluginSites(Collection<String> sites) {
1665 putCollection("pluginmanager.sites", sites);
1666 }
1667
1668 protected XMLStreamReader parser;
1669
1670 public static void validateXML(Reader in) throws IOException, SAXException {
1671 SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
1672 try (InputStream xsdStream = new CachedFile("resource://data/preferences.xsd").getInputStream()) {
1673 Schema schema = factory.newSchema(new StreamSource(xsdStream));
1674 Validator validator = schema.newValidator();
1675 validator.validate(new StreamSource(in));
1676 }
1677 }
1678
1679 protected void fromXML(Reader in) throws XMLStreamException {
1680 XMLStreamReader parser = XMLInputFactory.newInstance().createXMLStreamReader(in);
1681 this.parser = parser;
1682 parse();
1683 }
1684
1685 private void parse() throws XMLStreamException {
1686 int event = parser.getEventType();
1687 while (true) {
1688 if (event == XMLStreamConstants.START_ELEMENT) {
1689 try {
1690 loadedVersion = Integer.parseInt(parser.getAttributeValue(null, "version"));
1691 } catch (NumberFormatException e) {
1692 if (Main.isDebugEnabled()) {
1693 Main.debug(e.getMessage());
1694 }
1695 }
1696 parseRoot();
1697 } else if (event == XMLStreamConstants.END_ELEMENT) {
1698 return;
1699 }
1700 if (parser.hasNext()) {
1701 event = parser.next();
1702 } else {
1703 break;
1704 }
1705 }
1706 parser.close();
1707 }
1708
1709 private void parseRoot() throws XMLStreamException {
1710 while (true) {
1711 int event = parser.next();
1712 if (event == XMLStreamConstants.START_ELEMENT) {
1713 String localName = parser.getLocalName();
1714 switch(localName) {
1715 case "tag":
1716 settingsMap.put(parser.getAttributeValue(null, "key"), new StringSetting(parser.getAttributeValue(null, "value")));
1717 jumpToEnd();
1718 break;
1719 case "list":
1720 case "collection":
1721 case "lists":
1722 case "maps":
1723 parseToplevelList();
1724 break;
1725 default:
1726 throwException("Unexpected element: "+localName);
1727 }
1728 } else if (event == XMLStreamConstants.END_ELEMENT) {
1729 return;
1730 }
1731 }
1732 }
1733
1734 private void jumpToEnd() throws XMLStreamException {
1735 while (true) {
1736 int event = parser.next();
1737 if (event == XMLStreamConstants.START_ELEMENT) {
1738 jumpToEnd();
1739 } else if (event == XMLStreamConstants.END_ELEMENT) {
1740 return;
1741 }
1742 }
1743 }
1744
1745 private void parseToplevelList() throws XMLStreamException {
1746 String key = parser.getAttributeValue(null, "key");
1747 String name = parser.getLocalName();
1748
1749 List<String> entries = null;
1750 List<List<String>> lists = null;
1751 List<Map<String, String>> maps = null;
1752 while (true) {
1753 int event = parser.next();
1754 if (event == XMLStreamConstants.START_ELEMENT) {
1755 String localName = parser.getLocalName();
1756 switch(localName) {
1757 case "entry":
1758 if (entries == null) {
1759 entries = new ArrayList<>();
1760 }
1761 entries.add(parser.getAttributeValue(null, "value"));
1762 jumpToEnd();
1763 break;
1764 case "list":
1765 if (lists == null) {
1766 lists = new ArrayList<>();
1767 }
1768 lists.add(parseInnerList());
1769 break;
1770 case "map":
1771 if (maps == null) {
1772 maps = new ArrayList<>();
1773 }
1774 maps.add(parseMap());
1775 break;
1776 default:
1777 throwException("Unexpected element: "+localName);
1778 }
1779 } else if (event == XMLStreamConstants.END_ELEMENT) {
1780 break;
1781 }
1782 }
1783 if (entries != null) {
1784 settingsMap.put(key, new ListSetting(Collections.unmodifiableList(entries)));
1785 } else if (lists != null) {
1786 settingsMap.put(key, new ListListSetting(Collections.unmodifiableList(lists)));
1787 } else if (maps != null) {
1788 settingsMap.put(key, new MapListSetting(Collections.unmodifiableList(maps)));
1789 } else {
1790 if ("lists".equals(name)) {
1791 settingsMap.put(key, new ListListSetting(Collections.<List<String>>emptyList()));
1792 } else if ("maps".equals(name)) {
1793 settingsMap.put(key, new MapListSetting(Collections.<Map<String, String>>emptyList()));
1794 } else {
1795 settingsMap.put(key, new ListSetting(Collections.<String>emptyList()));
1796 }
1797 }
1798 }
1799
1800 private List<String> parseInnerList() throws XMLStreamException {
1801 List<String> entries = new ArrayList<>();
1802 while (true) {
1803 int event = parser.next();
1804 if (event == XMLStreamConstants.START_ELEMENT) {
1805 if ("entry".equals(parser.getLocalName())) {
1806 entries.add(parser.getAttributeValue(null, "value"));
1807 jumpToEnd();
1808 } else {
1809 throwException("Unexpected element: "+parser.getLocalName());
1810 }
1811 } else if (event == XMLStreamConstants.END_ELEMENT) {
1812 break;
1813 }
1814 }
1815 return Collections.unmodifiableList(entries);
1816 }
1817
1818 private Map<String, String> parseMap() throws XMLStreamException {
1819 Map<String, String> map = new LinkedHashMap<>();
1820 while (true) {
1821 int event = parser.next();
1822 if (event == XMLStreamConstants.START_ELEMENT) {
1823 if ("tag".equals(parser.getLocalName())) {
1824 map.put(parser.getAttributeValue(null, "key"), parser.getAttributeValue(null, "value"));
1825 jumpToEnd();
1826 } else {
1827 throwException("Unexpected element: "+parser.getLocalName());
1828 }
1829 } else if (event == XMLStreamConstants.END_ELEMENT) {
1830 break;
1831 }
1832 }
1833 return Collections.unmodifiableMap(map);
1834 }
1835
1836 protected void throwException(String msg) {
1837 throw new RuntimeException(msg + tr(" (at line {0}, column {1})",
1838 parser.getLocation().getLineNumber(), parser.getLocation().getColumnNumber()));
1839 }
1840
1841 private class SettingToXml implements SettingVisitor {
1842 private final StringBuilder b;
1843 private final boolean noPassword;
1844 private String key;
1845
1846 SettingToXml(StringBuilder b, boolean noPassword) {
1847 this.b = b;
1848 this.noPassword = noPassword;
1849 }
1850
1851 public void setKey(String key) {
1852 this.key = key;
1853 }
1854
1855 @Override
1856 public void visit(StringSetting setting) {
1857 if (noPassword && "osm-server.password".equals(key))
1858 return; // do not store plain password.
1859 /* don't save default values */
1860 if (setting.equals(defaultsMap.get(key)))
1861 return;
1862 b.append(" <tag key='");
1863 b.append(XmlWriter.encode(key));
1864 b.append("' value='");
1865 b.append(XmlWriter.encode(setting.getValue()));
1866 b.append("'/>\n");
1867 }
1868
1869 @Override
1870 public void visit(ListSetting setting) {
1871 /* don't save default values */
1872 if (setting.equals(defaultsMap.get(key)))
1873 return;
1874 b.append(" <list key='").append(XmlWriter.encode(key)).append("'>\n");
1875 for (String s : setting.getValue()) {
1876 b.append(" <entry value='").append(XmlWriter.encode(s)).append("'/>\n");
1877 }
1878 b.append(" </list>\n");
1879 }
1880
1881 @Override
1882 public void visit(ListListSetting setting) {
1883 /* don't save default values */
1884 if (setting.equals(defaultsMap.get(key)))
1885 return;
1886 b.append(" <lists key='").append(XmlWriter.encode(key)).append("'>\n");
1887 for (List<String> list : setting.getValue()) {
1888 b.append(" <list>\n");
1889 for (String s : list) {
1890 b.append(" <entry value='").append(XmlWriter.encode(s)).append("'/>\n");
1891 }
1892 b.append(" </list>\n");
1893 }
1894 b.append(" </lists>\n");
1895 }
1896
1897 @Override
1898 public void visit(MapListSetting setting) {
1899 b.append(" <maps key='").append(XmlWriter.encode(key)).append("'>\n");
1900 for (Map<String, String> struct : setting.getValue()) {
1901 b.append(" <map>\n");
1902 for (Entry<String, String> e : struct.entrySet()) {
1903 b.append(" <tag key='").append(XmlWriter.encode(e.getKey()))
1904 .append("' value='").append(XmlWriter.encode(e.getValue())).append("'/>\n");
1905 }
1906 b.append(" </map>\n");
1907 }
1908 b.append(" </maps>\n");
1909 }
1910 }
1911
1912 public String toXML(boolean nopass) {
1913 StringBuilder b = new StringBuilder(
1914 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<preferences xmlns=\"")
1915 .append(Main.getXMLBase()).append("/preferences-1.0\" version=\"")
1916 .append(Version.getInstance().getVersion()).append("\">\n");
1917 SettingToXml toXml = new SettingToXml(b, nopass);
1918 for (Entry<String, Setting<?>> e : settingsMap.entrySet()) {
1919 toXml.setKey(e.getKey());
1920 e.getValue().visit(toXml);
1921 }
1922 b.append("</preferences>\n");
1923 return b.toString();
1924 }
1925
1926 /**
1927 * Removes obsolete preference settings. If you throw out a once-used preference
1928 * setting, add it to the list here with an expiry date (written as comment). If you
1929 * see something with an expiry date in the past, remove it from the list.
1930 */
1931 public void removeObsolete() {
1932 // drop this block march 2016
1933 // update old style JOSM server links to use zip now, see #10581, #12189
1934 // actually also cache and mirror entries should be cleared
1935 if (loadedVersion < 9216) {
1936 for (String key: new String[]{"mappaint.style.entries", "taggingpreset.entries"}) {
1937 Collection<Map<String, String>> data = getListOfStructs(key, (Collection<Map<String, String>>) null);
1938 if (data != null) {
1939 List<Map<String, String>> newlist = new ArrayList<>();
1940 boolean modified = false;
1941 for (Map<String, String> map : data) {
1942 Map<String, String> newmap = new LinkedHashMap<>();
1943 for (Entry<String, String> entry : map.entrySet()) {
1944 String val = entry.getValue();
1945 String mkey = entry.getKey();
1946 if ("url".equals(mkey) && val.contains("josm.openstreetmap.de/josmfile") && !val.contains("zip=1")) {
1947 val += "&zip=1";
1948 modified = true;
1949 }
1950 if ("url".equals(mkey) && val.contains("http://josm.openstreetmap.de/josmfile")) {
1951 val = val.replace("http://", "https://");
1952 modified = true;
1953 }
1954 newmap.put(mkey, val);
1955 }
1956 newlist.add(newmap);
1957 }
1958 if (modified) {
1959 putListOfStructs(key, newlist);
1960 }
1961 }
1962 }
1963 }
1964
1965 for (String key : OBSOLETE_PREF_KEYS) {
1966 if (settingsMap.containsKey(key)) {
1967 settingsMap.remove(key);
1968 Main.info(tr("Preference setting {0} has been removed since it is no longer used.", key));
1969 }
1970 }
1971 }
1972
1973 /**
1974 * Enables or not the preferences file auto-save mechanism (save each time a setting is changed).
1975 * This behaviour is enabled by default.
1976 * @param enable if {@code true}, makes JOSM save preferences file each time a setting is changed
1977 * @since 7085
1978 */
1979 public final void enableSaveOnPut(boolean enable) {
1980 synchronized (this) {
1981 saveOnPut = enable;
1982 }
1983 }
1984}
Note: See TracBrowser for help on using the repository browser.