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

Last change on this file since 12656 was 12649, checked in by Don-vip, 7 years ago

see #15182 - code refactoring to avoid dependence on GUI packages from Preferences

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