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

Last change on this file was 19536, checked in by stoecker, 7 weeks ago

remove PMD ImplicitFunctionalInterface, see #24635

  • Property svn:eol-style set to native
File size: 39.1 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5import static org.openstreetmap.josm.tools.Utils.getSystemEnv;
6import static org.openstreetmap.josm.tools.Utils.getSystemProperty;
7
8import java.awt.GraphicsEnvironment;
9import java.io.File;
10import java.io.IOException;
11import java.io.PrintWriter;
12import java.io.Reader;
13import java.io.StringWriter;
14import java.nio.charset.StandardCharsets;
15import java.nio.file.Files;
16import java.nio.file.InvalidPathException;
17import java.nio.file.StandardCopyOption;
18import java.util.ArrayList;
19import java.util.Arrays;
20import java.util.Collection;
21import java.util.Collections;
22import java.util.HashMap;
23import java.util.HashSet;
24import java.util.List;
25import java.util.Map;
26import java.util.Map.Entry;
27import java.util.Optional;
28import java.util.Set;
29import java.util.SortedMap;
30import java.util.TreeMap;
31import java.util.concurrent.TimeUnit;
32import java.util.stream.Collectors;
33import java.util.stream.Stream;
34
35import javax.swing.JOptionPane;
36import javax.xml.stream.XMLStreamException;
37
38import org.openstreetmap.josm.data.preferences.ColorInfo;
39import org.openstreetmap.josm.data.preferences.JosmBaseDirectories;
40import org.openstreetmap.josm.data.preferences.JosmUrls;
41import org.openstreetmap.josm.data.preferences.NamedColorProperty;
42import org.openstreetmap.josm.data.preferences.PreferencesReader;
43import org.openstreetmap.josm.data.preferences.PreferencesWriter;
44import org.openstreetmap.josm.gui.MainApplication;
45import org.openstreetmap.josm.io.NetworkManager;
46import org.openstreetmap.josm.io.OsmApi;
47import org.openstreetmap.josm.spi.preferences.AbstractPreferences;
48import org.openstreetmap.josm.spi.preferences.Config;
49import org.openstreetmap.josm.spi.preferences.DefaultPreferenceChangeEvent;
50import org.openstreetmap.josm.spi.preferences.IBaseDirectories;
51import org.openstreetmap.josm.spi.preferences.ListSetting;
52import org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent;
53import org.openstreetmap.josm.spi.preferences.Setting;
54import org.openstreetmap.josm.tools.CheckParameterUtil;
55import org.openstreetmap.josm.tools.ListenerList;
56import org.openstreetmap.josm.tools.Logging;
57import org.openstreetmap.josm.tools.PlatformManager;
58import org.openstreetmap.josm.tools.ReflectionUtils;
59import org.openstreetmap.josm.tools.Utils;
60import org.xml.sax.SAXException;
61
62/**
63 * This class holds all preferences for JOSM.
64 *
65 * Other classes can register their beloved properties here. All properties will be
66 * saved upon set-access.
67 *
68 * Each property is a key=setting pair, where key is a String and setting can be one of
69 * 4 types:
70 * string, list, list of lists and list of maps.
71 * In addition, each key has a unique default value that is set when the value is first
72 * accessed using one of the get...() methods. You can use the same preference
73 * key in different parts of the code, but the default value must be the same
74 * everywhere. A default value of null means, the setting has been requested, but
75 * no default value was set. This is used in advanced preferences to present a list
76 * off all possible settings.
77 *
78 * At the moment, you cannot put the empty string for string properties.
79 * put(key, "") means, the property is removed.
80 *
81 * @author imi
82 * @since 74
83 */
84public class Preferences extends AbstractPreferences {
85
86 /** remove if key equals */
87 private static final String[] OBSOLETE_PREF_KEYS = {
88 // nothing ATM
89 };
90
91 /** remove if key starts with */
92 private static final String[] OBSOLETE_PREF_KEYS_START = {
93 // nothing ATM
94 };
95
96 /** keep subkey even if it starts with any of {@link #OBSOLETE_PREF_KEYS_START} */
97 private static final List<String> KEEP_PREF_KEYS = Arrays.asList(
98 // nothing ATM
99 );
100
101 /** rename keys that equal */
102 private static final Map<String, String> UPDATE_PREF_KEYS = getUpdatePrefKeys();
103
104 private static Map<String, String> getUpdatePrefKeys() {
105 HashMap<String, String> m = new HashMap<>();
106 m.put("hdop.color.alpha", "circle.color.alpha");
107 m.put("points.hdopcircle", "points.circle");
108 return Collections.unmodifiableMap(m);
109 }
110
111 private static final long MAX_AGE_DEFAULT_PREFERENCES = TimeUnit.DAYS.toSeconds(50);
112
113 private final IBaseDirectories dirs;
114 boolean modifiedDefault;
115
116 /**
117 * Determines if preferences file is saved each time a property is changed.
118 */
119 private boolean saveOnPut = true;
120
121 /**
122 * Maps the setting name to the current value of the setting.
123 * The map must not contain null as key or value. The mapped setting objects
124 * must not have a null value.
125 */
126 protected final SortedMap<String, Setting<?>> settingsMap = new TreeMap<>();
127
128 /**
129 * Maps the setting name to the default value of the setting.
130 * The map must not contain null as key or value. The value of the mapped
131 * setting objects can be null.
132 */
133 protected final SortedMap<String, Setting<?>> defaultsMap = new TreeMap<>();
134
135 /**
136 * Indicates whether {@link #init(boolean)} completed successfully.
137 * Used to decide whether to write backup preference file in {@link #save()}
138 */
139 protected boolean initSuccessful;
140
141 private final ListenerList<org.openstreetmap.josm.spi.preferences.PreferenceChangedListener> listeners = ListenerList.create();
142
143 private final HashMap<String, ListenerList<org.openstreetmap.josm.spi.preferences.PreferenceChangedListener>> keyListeners = new HashMap<>();
144
145 private static final Preferences defaultInstance = new Preferences(JosmBaseDirectories.getInstance());
146
147 /**
148 * Preferences classes calling directly the method {@link #putSetting(String, Setting)}.
149 * This collection allows us to exclude them when searching the business class who set a preference.
150 * The found class is used as event source when notifying event listeners.
151 */
152 private static final Collection<Class<?>> preferencesClasses = Arrays.asList(
153 Preferences.class, PreferencesUtils.class, AbstractPreferences.class);
154
155 /**
156 * Constructs a new {@code Preferences}.
157 */
158 public Preferences() {
159 this.dirs = Config.getDirs();
160 }
161
162 /**
163 * Constructs a new {@code Preferences}.
164 *
165 * @param dirs the directories to use for saving the preferences
166 */
167 public Preferences(IBaseDirectories dirs) {
168 this.dirs = dirs;
169 }
170
171 /**
172 * Constructs a new {@code Preferences} from an existing instance.
173 * @param pref existing preferences to copy
174 * @since 12634
175 */
176 public Preferences(Preferences pref) {
177 this(pref.dirs);
178 settingsMap.putAll(pref.settingsMap);
179 defaultsMap.putAll(pref.defaultsMap);
180 }
181
182 /**
183 * Returns the main (default) preferences instance.
184 * @return the main (default) preferences instance
185 * @since 14149
186 */
187 public static Preferences main() {
188 return defaultInstance;
189 }
190
191 /**
192 * Adds a new preferences listener.
193 * @param listener The listener to add
194 * @since 12881
195 */
196 @Override
197 public void addPreferenceChangeListener(org.openstreetmap.josm.spi.preferences.PreferenceChangedListener listener) {
198 if (listener != null) {
199 listeners.addListener(listener);
200 }
201 }
202
203 /**
204 * Removes a preferences listener.
205 * @param listener The listener to remove
206 * @since 12881
207 */
208 @Override
209 public void removePreferenceChangeListener(org.openstreetmap.josm.spi.preferences.PreferenceChangedListener listener) {
210 listeners.removeListener(listener);
211 }
212
213 /**
214 * Adds a listener that only listens to changes in one preference
215 * @param key The preference key to listen to
216 * @param listener The listener to add.
217 * @since 12881
218 */
219 @Override
220 public void addKeyPreferenceChangeListener(String key, org.openstreetmap.josm.spi.preferences.PreferenceChangedListener listener) {
221 listenersForKey(key).addListener(listener);
222 }
223
224 /**
225 * Adds a weak listener that only listens to changes in one preference
226 * @param key The preference key to listen to
227 * @param listener The listener to add.
228 * @since 10824
229 */
230 public void addWeakKeyPreferenceChangeListener(String key, org.openstreetmap.josm.spi.preferences.PreferenceChangedListener listener) {
231 listenersForKey(key).addWeakListener(listener);
232 }
233
234 private ListenerList<org.openstreetmap.josm.spi.preferences.PreferenceChangedListener> listenersForKey(String key) {
235 return keyListeners.computeIfAbsent(key, k -> ListenerList.create());
236 }
237
238 /**
239 * Removes a listener that only listens to changes in one preference
240 * @param key The preference key to listen to
241 * @param listener The listener to add.
242 * @since 12881
243 */
244 @Override
245 public void removeKeyPreferenceChangeListener(String key, org.openstreetmap.josm.spi.preferences.PreferenceChangedListener listener) {
246 Optional.ofNullable(keyListeners.get(key)).orElseThrow(
247 () -> new IllegalArgumentException("There are no listeners registered for " + key))
248 .removeListener(listener);
249 }
250
251 protected void firePreferenceChanged(String key, Setting<?> oldValue, Setting<?> newValue) {
252 final Class<?> source = ReflectionUtils.findCallerClass(preferencesClasses);
253 final PreferenceChangeEvent evt =
254 new DefaultPreferenceChangeEvent(source != null ? source : getClass(), key, oldValue, newValue);
255 listeners.fireEvent(listener -> listener.preferenceChanged(evt));
256
257 ListenerList<org.openstreetmap.josm.spi.preferences.PreferenceChangedListener> forKey = keyListeners.get(key);
258 if (forKey != null) {
259 forKey.fireEvent(listener -> listener.preferenceChanged(evt));
260 }
261 }
262
263 /**
264 * Get the base name of the JOSM directories for preferences, cache and user data.
265 * Default value is "JOSM", unless overridden by system property "josm.dir.name".
266 * @return the base name of the JOSM directories for preferences, cache and user data
267 */
268 public static String getJOSMDirectoryBaseName() {
269 String name = getSystemProperty("josm.dir.name");
270 if (name != null)
271 return name;
272 else
273 return "JOSM";
274 }
275
276 /**
277 * Get the base directories associated with this preference instance.
278 * @return the base directories
279 */
280 public IBaseDirectories getDirs() {
281 return dirs;
282 }
283
284 /**
285 * Returns the user preferences file (preferences.xml).
286 * @return The user preferences file (preferences.xml)
287 */
288 public File getPreferenceFile() {
289 return new File(dirs.getPreferencesDirectory(false), "preferences.xml");
290 }
291
292 /**
293 * Returns the cache file for default preferences.
294 * @return the cache file for default preferences
295 */
296 public File getDefaultsCacheFile() {
297 return new File(dirs.getCacheDirectory(true), "default_preferences.xml");
298 }
299
300 /**
301 * Returns the user plugin directory.
302 * @return The user plugin directory
303 */
304 public File getPluginsDirectory() {
305 return new File(dirs.getUserDataDirectory(false), "plugins");
306 }
307
308 private static void addPossibleResourceDir(Set<String> locations, String s) {
309 if (s != null) {
310 if (!s.endsWith(File.separator)) {
311 s += File.separator;
312 }
313 locations.add(s);
314 }
315 }
316
317 /**
318 * Returns a set of all existing directories where resources could be stored.
319 * @return A set of all existing directories where resources could be stored.
320 */
321 public static Collection<String> getAllPossiblePreferenceDirs() {
322 Set<String> locations = new HashSet<>();
323 addPossibleResourceDir(locations, defaultInstance.dirs.getPreferencesDirectory(false).getPath());
324 addPossibleResourceDir(locations, defaultInstance.dirs.getUserDataDirectory(false).getPath());
325 addPossibleResourceDir(locations, getSystemEnv("JOSM_RESOURCES"));
326 addPossibleResourceDir(locations, getSystemProperty("josm.resources"));
327 locations.addAll(PlatformManager.getPlatform().getPossiblePreferenceDirs());
328 return locations;
329 }
330
331 /**
332 * Get all named colors, including customized and the default ones.
333 * @return a map of all named colors (maps preference key to {@link ColorInfo})
334 */
335 public synchronized Map<String, ColorInfo> getAllNamedColors() {
336 final Map<String, ColorInfo> all = new TreeMap<>();
337 for (final Entry<String, Setting<?>> e : settingsMap.entrySet()) {
338 if (!e.getKey().startsWith(NamedColorProperty.NAMED_COLOR_PREFIX))
339 continue;
340 Utils.instanceOfAndCast(e.getValue(), ListSetting.class)
341 .map(ListSetting::getValue)
342 .map(lst -> ColorInfo.fromPref(lst, false))
343 .ifPresent(info -> all.put(e.getKey(), info));
344 }
345 for (final Entry<String, Setting<?>> e : defaultsMap.entrySet()) {
346 if (!e.getKey().startsWith(NamedColorProperty.NAMED_COLOR_PREFIX))
347 continue;
348 Utils.instanceOfAndCast(e.getValue(), ListSetting.class)
349 .map(ListSetting::getValue)
350 .map(lst -> ColorInfo.fromPref(lst, true))
351 .ifPresent(infoDef -> {
352 ColorInfo info = all.get(e.getKey());
353 if (info == null) {
354 all.put(e.getKey(), infoDef);
355 } else {
356 info.setDefaultValue(infoDef.getDefaultValue());
357 }
358 });
359 }
360 return all;
361 }
362
363 /**
364 * Called after every put. In case of a problem, do nothing but output the error in log.
365 * @throws IOException if any I/O error occurs
366 */
367 public synchronized void save() throws IOException {
368 save(getPreferenceFile(), settingsMap.entrySet().stream().filter(e -> !e.getValue().equals(defaultsMap.get(e.getKey()))), false);
369 }
370
371 /**
372 * Stores the defaults to the defaults file
373 * @throws IOException If the file could not be saved
374 */
375 public synchronized void saveDefaults() throws IOException {
376 save(getDefaultsCacheFile(), defaultsMap.entrySet().stream(), true);
377 }
378
379 protected void save(File prefFile, Stream<Entry<String, Setting<?>>> settings, boolean defaults) throws IOException {
380 if (!defaults) {
381 /* currently unused, but may help to fix configuration issues in future */
382 putInt("josm.version", Version.getInstance().getVersion());
383 }
384
385 File backupFile = new File(prefFile + "_backup");
386
387 // Backup old preferences if there are old preferences
388 if (initSuccessful && prefFile.exists() && prefFile.length() > 0) {
389 checkFileValidity(prefFile, f -> Utils.copyFile(f, backupFile));
390 }
391
392 try (PreferencesWriter writer = new PreferencesWriter(
393 new PrintWriter(prefFile + "_tmp", StandardCharsets.UTF_8), false, defaults)) {
394 writer.write(settings);
395 } catch (SecurityException e) {
396 throw new IOException(e);
397 }
398
399 File tmpFile = new File(prefFile + "_tmp");
400 // Only replace the pref file if the _tmp file is valid
401 checkFileValidity(tmpFile, f -> Files.move(f.toPath(), prefFile.toPath(), StandardCopyOption.REPLACE_EXISTING));
402
403 setCorrectPermissions(prefFile);
404 setCorrectPermissions(backupFile);
405 }
406
407 /**
408 * Ensure that a preferences file is "ok" before copying/moving it over another preferences file
409 * @param file The file to check
410 * @param consumer The consumer that will perform the copy/move action
411 * @throws IOException If there is an issue reading/writing the file
412 */
413 private static void checkFileValidity(File file, ThrowingConsumer<File, IOException> consumer) throws IOException {
414 try {
415 // But don't back up if the current preferences are invalid.
416 // The validations are expensive (~2/3 CPU, ~1/3 memory), but this isn't a "hot" method
417 PreferencesReader.validateXML(file);
418 PreferencesReader reader = new PreferencesReader(file, false);
419 reader.parse();
420 consumer.accept(file);
421 } catch (SAXException | XMLStreamException e) {
422 Logging.trace(e);
423 Logging.debug("Invalid preferences file (" + file + ") due to: " + e.getMessage());
424 }
425 }
426
427 private static void setCorrectPermissions(File file) {
428 if (!file.setReadable(false, false) && Logging.isTraceEnabled()) {
429 Logging.trace(tr("Unable to set file non-readable {0}", file.getAbsolutePath()));
430 }
431 if (!file.setWritable(false, false) && Logging.isTraceEnabled()) {
432 Logging.trace(tr("Unable to set file non-writable {0}", file.getAbsolutePath()));
433 }
434 if (!file.setExecutable(false, false) && Logging.isTraceEnabled()) {
435 Logging.trace(tr("Unable to set file non-executable {0}", file.getAbsolutePath()));
436 }
437 if (!file.setReadable(true, true) && Logging.isTraceEnabled()) {
438 Logging.trace(tr("Unable to set file readable {0}", file.getAbsolutePath()));
439 }
440 if (!file.setWritable(true, true) && Logging.isTraceEnabled()) {
441 Logging.trace(tr("Unable to set file writable {0}", file.getAbsolutePath()));
442 }
443 }
444
445 /**
446 * Loads preferences from settings file.
447 * @throws IOException if any I/O error occurs while reading the file
448 * @throws SAXException if the settings file does not contain valid XML
449 * @throws XMLStreamException if an XML error occurs while parsing the file (after validation)
450 */
451 protected void load() throws IOException, SAXException, XMLStreamException {
452 File pref = getPreferenceFile();
453 PreferencesReader.validateXML(pref);
454 PreferencesReader reader = new PreferencesReader(pref, false);
455 reader.parse();
456 settingsMap.clear();
457 settingsMap.putAll(reader.getSettings());
458 removeAndUpdateObsolete(reader.getVersion());
459 }
460
461 /**
462 * Loads default preferences from default settings cache file.
463 *
464 * Discards entries older than {@link #MAX_AGE_DEFAULT_PREFERENCES}.
465 *
466 * @throws IOException if any I/O error occurs while reading the file
467 * @throws SAXException if the settings file does not contain valid XML
468 * @throws XMLStreamException if an XML error occurs while parsing the file (after validation)
469 */
470 protected void loadDefaults() throws IOException, XMLStreamException, SAXException {
471 File def = getDefaultsCacheFile();
472 PreferencesReader.validateXML(def);
473 PreferencesReader reader = new PreferencesReader(def, true);
474 reader.parse();
475 defaultsMap.clear();
476 long minTime = System.currentTimeMillis() / 1000 - MAX_AGE_DEFAULT_PREFERENCES;
477 for (Entry<String, Setting<?>> e : reader.getSettings().entrySet()) {
478 if (e.getValue().getTime() >= minTime) {
479 defaultsMap.put(e.getKey(), e.getValue());
480 }
481 }
482 }
483
484 /**
485 * Loads preferences from XML reader.
486 * @param in XML reader
487 * @throws XMLStreamException if any XML stream error occurs
488 * @throws IOException if any I/O error occurs
489 */
490 public synchronized void fromXML(Reader in) throws XMLStreamException, IOException {
491 PreferencesReader reader = new PreferencesReader(in, false);
492 reader.parse();
493 settingsMap.clear();
494 settingsMap.putAll(reader.getSettings());
495 }
496
497 /**
498 * Initializes preferences.
499 * @param reset if {@code true}, current settings file is replaced by the default one
500 */
501 public synchronized void init(boolean reset) {
502 initSuccessful = false;
503 // get the preferences.
504 File prefDir = dirs.getPreferencesDirectory(false);
505 if (prefDir.exists()) {
506 if (!prefDir.isDirectory()) {
507 Logging.warn(tr("Failed to initialize preferences. Preference directory ''{0}'' is not a directory.",
508 prefDir.getAbsoluteFile()));
509 if (!GraphicsEnvironment.isHeadless()) {
510 JOptionPane.showMessageDialog(
511 MainApplication.getMainFrame(),
512 tr("<html>Failed to initialize preferences.<br>Preference directory ''{0}'' is not a directory.</html>",
513 prefDir.getAbsoluteFile()),
514 tr("Error"),
515 JOptionPane.ERROR_MESSAGE
516 );
517 }
518 return;
519 }
520 } else {
521 if (!prefDir.mkdirs()) {
522 Logging.warn(tr("Failed to initialize preferences. Failed to create missing preference directory: {0}",
523 prefDir.getAbsoluteFile()));
524 if (!GraphicsEnvironment.isHeadless()) {
525 JOptionPane.showMessageDialog(
526 MainApplication.getMainFrame(),
527 tr("<html>Failed to initialize preferences.<br>Failed to create missing preference directory: {0}</html>",
528 prefDir.getAbsoluteFile()),
529 tr("Error"),
530 JOptionPane.ERROR_MESSAGE
531 );
532 }
533 return;
534 }
535 }
536
537 File preferenceFile = getPreferenceFile();
538 try {
539 if (!preferenceFile.exists()) {
540 Logging.info(tr("Missing preference file ''{0}''. Creating a default preference file.", preferenceFile.getAbsoluteFile()));
541 resetToDefault();
542 save();
543 } else if (reset) {
544 File backupFile = new File(prefDir, "preferences.xml.bak");
545 PlatformManager.getPlatform().rename(preferenceFile, backupFile);
546 Logging.warn(tr("Replacing existing preference file ''{0}'' with default preference file.", preferenceFile.getAbsoluteFile()));
547 resetToDefault();
548 save();
549 }
550 } catch (IOException | InvalidPathException e) {
551 Logging.error(e);
552 if (!GraphicsEnvironment.isHeadless()) {
553 JOptionPane.showMessageDialog(
554 MainApplication.getMainFrame(),
555 tr("<html>Failed to initialize preferences.<br>Failed to reset preference file to default: {0}</html>",
556 getPreferenceFile().getAbsoluteFile()),
557 tr("Error"),
558 JOptionPane.ERROR_MESSAGE
559 );
560 }
561 return;
562 }
563 File def = getDefaultsCacheFile();
564 if (def.exists()) {
565 try {
566 loadDefaults();
567 } catch (IOException | XMLStreamException | SAXException e) {
568 Logging.error(e);
569 Logging.warn(tr("Failed to load defaults cache file: {0}", def));
570 defaultsMap.clear();
571 if (!def.delete()) {
572 Logging.warn(tr("Failed to delete faulty defaults cache file: {0}", def));
573 }
574 }
575 }
576 File possiblyGoodBackupFile = new File(prefDir, "preferences.xml_backup");
577 try {
578 load();
579 initSuccessful = true;
580 } catch (IOException | SAXException | XMLStreamException e) {
581 Logging.error(e);
582 File backupFile = new File(prefDir, "preferences.xml.bak");
583 if (!GraphicsEnvironment.isHeadless()) {
584 JOptionPane.showMessageDialog(
585 MainApplication.getMainFrame(),
586 tr("<html>Preferences file had errors.<br> Making backup of old one to <br>{0}<br> " +
587 "and trying to read last good preference file <br>{1}<br>.</html>",
588 backupFile.getAbsoluteFile(), possiblyGoodBackupFile.getAbsoluteFile()),
589 tr("Error"),
590 JOptionPane.ERROR_MESSAGE
591 );
592 }
593 PlatformManager.getPlatform().rename(preferenceFile, backupFile);
594 }
595 if (!initSuccessful) {
596 try {
597 if (possiblyGoodBackupFile.exists() && possiblyGoodBackupFile.length() > 0) {
598 Utils.copyFile(possiblyGoodBackupFile, preferenceFile);
599 }
600
601 load();
602 initSuccessful = true;
603 } catch (IOException | SAXException | XMLStreamException e) {
604 Logging.error(e);
605 Logging.warn(tr("Failed to initialize preferences. Failed to reset preference file to default: {0}", getPreferenceFile()));
606 }
607 }
608 if (!initSuccessful) {
609 try {
610 if (!GraphicsEnvironment.isHeadless()) {
611 JOptionPane.showMessageDialog(
612 MainApplication.getMainFrame(),
613 tr("<html>Preferences file had errors.<br> Creating a new default preference file.</html>"),
614 tr("Error"),
615 JOptionPane.ERROR_MESSAGE
616 );
617 }
618 resetToDefault();
619 save();
620 } catch (IOException e1) {
621 Logging.error(e1);
622 Logging.warn(tr("Failed to initialize preferences. Failed to reset preference file to default: {0}", getPreferenceFile()));
623 }
624 }
625 }
626
627 /**
628 * Resets the preferences to their initial state. This resets all values and file associations.
629 * The default values and listeners are not removed.
630 * <p>
631 * It is meant to be called before {@link #init(boolean)}
632 * @since 10876
633 */
634 public void resetToInitialState() {
635 resetToDefault();
636 saveOnPut = true;
637 initSuccessful = false;
638 }
639
640 /**
641 * Reset all values stored in this map to the default values. This clears the preferences.
642 */
643 public final synchronized void resetToDefault() {
644 settingsMap.clear();
645 }
646
647 /**
648 * Set a value for a certain setting. The changed setting is saved to the preference file immediately.
649 * Due to caching mechanisms on modern operating systems and hardware, this shouldn't be a performance problem.
650 * @param key the unique identifier for the setting
651 * @param setting the value of the setting. In case it is null, the key-value entry will be removed.
652 * @return {@code true}, if something has changed (i.e. value is different than before)
653 */
654 @Override
655 public boolean putSetting(final String key, Setting<?> setting) {
656 CheckParameterUtil.ensureParameterNotNull(key);
657 if (setting != null && setting.getValue() == null)
658 throw new IllegalArgumentException("setting argument must not have null value");
659 Setting<?> settingOld;
660 Setting<?> settingCopy = null;
661 synchronized (this) {
662 if (setting == null) {
663 settingOld = settingsMap.remove(key);
664 if (settingOld == null)
665 return false;
666 } else {
667 settingOld = settingsMap.get(key);
668 if (setting.equals(settingOld))
669 return false;
670 if (settingOld == null && setting.equals(defaultsMap.get(key)))
671 return false;
672 settingCopy = setting.copy();
673 settingsMap.put(key, settingCopy);
674 }
675 if (saveOnPut) {
676 try {
677 save();
678 } catch (IOException | InvalidPathException e) {
679 File file = getPreferenceFile();
680 try {
681 file = file.getAbsoluteFile();
682 } catch (SecurityException ex) {
683 Logging.trace(ex);
684 }
685 Logging.log(Logging.LEVEL_WARN, tr("Failed to persist preferences to ''{0}''", file), e);
686 }
687 }
688 }
689 // Call outside of synchronized section in case some listener wait for other thread that wait for preference lock
690 firePreferenceChanged(key, settingOld, settingCopy);
691 return true;
692 }
693
694 /**
695 * Get a setting of any type
696 * @param key The key for the setting
697 * @param def The default value to use if it was not found
698 * @return The setting
699 */
700 public synchronized Setting<?> getSetting(String key, Setting<?> def) {
701 return getSetting(key, def, Setting.class);
702 }
703
704 /**
705 * Get settings value for a certain key and provide default a value.
706 * @param <T> the setting type
707 * @param key the identifier for the setting
708 * @param def the default value. For each call of getSetting() with a given key, the default value must be the same.
709 * <code>def</code> must not be null, but the value of <code>def</code> can be null.
710 * @param klass the setting type (same as T)
711 * @return the corresponding value if the property has been set before, {@code def} otherwise
712 */
713 @SuppressWarnings("unchecked")
714 @Override
715 public synchronized <T extends Setting<?>> T getSetting(String key, T def, Class<T> klass) {
716 CheckParameterUtil.ensureParameterNotNull(key);
717 CheckParameterUtil.ensureParameterNotNull(def);
718 Setting<?> oldDef = defaultsMap.get(key);
719 if (oldDef != null && oldDef.isNew() && oldDef.getValue() != null && def.getValue() != null && !def.equals(oldDef)) {
720 Logging.info("Defaults for " + key + " differ: " + def + " != " + defaultsMap.get(key));
721 }
722 if (def.getValue() != null || oldDef == null) {
723 Setting<?> defCopy = def.copy();
724 defCopy.setTime(System.currentTimeMillis() / 1000);
725 defCopy.setNew(true);
726 defaultsMap.put(key, defCopy);
727 }
728 Setting<?> prop = settingsMap.get(key);
729 if (klass.isInstance(prop)) {
730 return (T) prop;
731 } else {
732 return def;
733 }
734 }
735
736 @Override
737 public Set<String> getKeySet() {
738 return Collections.unmodifiableSet(settingsMap.keySet());
739 }
740
741 @Override
742 public Map<String, Setting<?>> getAllSettings() {
743 return new TreeMap<>(settingsMap);
744 }
745
746 /**
747 * Gets a map of all currently known defaults
748 * @return The map (key/setting)
749 */
750 public Map<String, Setting<?>> getAllDefaults() {
751 return new TreeMap<>(defaultsMap);
752 }
753
754 /**
755 * Replies the collection of plugin site URLs from where plugin lists can be downloaded.
756 * @return the collection of plugin site URLs
757 * @see #getOnlinePluginSites
758 */
759 public Collection<String> getPluginSites() {
760 return getList("pluginmanager.sites", Collections.singletonList(Config.getUrls().getJOSMWebsite()+"/pluginicons%<?plugins=>"));
761 }
762
763 /**
764 * Returns the list of plugin sites available according to offline mode settings.
765 * @return the list of available plugin sites
766 * @since 8471
767 */
768 public Collection<String> getOnlinePluginSites() {
769 Collection<String> pluginSites = new ArrayList<>(getPluginSites());
770 pluginSites.removeIf(NetworkManager::isOffline);
771 return pluginSites;
772 }
773
774 /**
775 * Sets the collection of plugin site URLs.
776 *
777 * @param sites the site URLs
778 */
779 public void setPluginSites(Collection<String> sites) {
780 putList("pluginmanager.sites", new ArrayList<>(sites));
781 }
782
783 /**
784 * Returns XML describing these preferences.
785 * @param nopass if password must be excluded
786 * @return XML
787 */
788 public synchronized String toXML(boolean nopass) {
789 return toXML(settingsMap.entrySet(), nopass, false);
790 }
791
792 /**
793 * Returns XML describing the given preferences.
794 * @param settings preferences settings
795 * @param nopass if password must be excluded
796 * @param defaults true, if default values are converted to XML, false for
797 * regular preferences
798 * @return XML
799 */
800 public String toXML(Collection<Entry<String, Setting<?>>> settings, boolean nopass, boolean defaults) {
801 try (
802 StringWriter sw = new StringWriter();
803 PreferencesWriter prefWriter = new PreferencesWriter(new PrintWriter(sw), nopass, defaults)
804 ) {
805 prefWriter.write(settings);
806 sw.flush();
807 return sw.toString();
808 } catch (IOException e) {
809 Logging.error(e);
810 return null;
811 }
812 }
813
814 /**
815 * Removes and updates obsolete preference settings. If you throw out a once-used preference
816 * setting, add it to the list here with an expiry date (written as comment). If you
817 * see something with an expiry date in the past, remove it from the list.
818 * @param loadedVersion JOSM version when the preferences file was written
819 */
820 private void removeAndUpdateObsolete(int loadedVersion) {
821 Logging.trace("Update obsolete preference keys for version {0}", Integer.toString(loadedVersion));
822 for (Entry<String, String> e : UPDATE_PREF_KEYS.entrySet()) {
823 String oldkey = e.getKey();
824 String newkey = e.getValue();
825 if (settingsMap.containsKey(oldkey)) {
826 Setting<?> value = settingsMap.remove(oldkey);
827 settingsMap.putIfAbsent(newkey, value);
828 Logging.info(tr("Updated preference setting {0} to {1}", oldkey, newkey));
829 }
830 }
831
832 Logging.trace("Remove obsolete preferences for version {0}", Integer.toString(loadedVersion));
833 for (String key : OBSOLETE_PREF_KEYS) {
834 if (settingsMap.containsKey(key)) {
835 settingsMap.remove(key);
836 Logging.info(tr("Removed preference setting {0} since it is no longer used", key));
837 }
838 if (defaultsMap.containsKey(key)) {
839 defaultsMap.remove(key);
840 Logging.info(tr("Removed preference default {0} since it is no longer used", key));
841 modifiedDefault = true;
842 }
843 }
844 for (String key : OBSOLETE_PREF_KEYS_START) {
845 settingsMap.entrySet().stream()
846 .filter(e -> e.getKey().startsWith(key))
847 .collect(Collectors.toSet())
848 .forEach(e -> {
849 String k = e.getKey();
850 if (!KEEP_PREF_KEYS.contains(k)) {
851 settingsMap.remove(k);
852 Logging.info(tr("Removed preference setting {0} since it is no longer used", k));
853 }
854 });
855 defaultsMap.entrySet().stream()
856 .filter(e -> e.getKey().startsWith(key))
857 .collect(Collectors.toSet())
858 .forEach(e -> {
859 String k = e.getKey();
860 if (!KEEP_PREF_KEYS.contains(k)) {
861 defaultsMap.remove(k);
862 Logging.info(tr("Removed preference default {0} since it is no longer used", k));
863 modifiedDefault = true;
864 }
865 });
866 }
867 if (!getBoolean("preferences.reset.draw.rawgps.lines")) {
868 // see #18444
869 // add "preferences.reset.draw.rawgps.lines" to OBSOLETE_PREF_KEYS when removing
870 putBoolean("preferences.reset.draw.rawgps.lines", true);
871 putInt("draw.rawgps.lines", -1);
872 }
873 updateMapPaintKnownDefaults();
874 if (modifiedDefault) {
875 try {
876 saveDefaults();
877 Logging.info(tr("Saved updated default preferences."));
878 } catch (IOException ex) {
879 Logging.log(Logging.LEVEL_WARN, tr("Failed to save default preferences."), ex);
880 }
881 modifiedDefault = false;
882 }
883 // As of June 1st, 2024, the OSM.org instance no longer allows basic authentication.
884 if (JosmUrls.getInstance().getDefaultOsmApiUrl().equals(OsmApi.getOsmApi().getServerUrl()) && "basic".equals(OsmApi.getAuthMethod())) {
885 put("osm-server.auth-method", null);
886 put("osm-server.username", null);
887 put("osm-server.password", null);
888 }
889 }
890
891 /**
892 * Update the known defaults for the map paintstyles.
893 * This should be removed sometime after 2024-06-01.
894 */
895 private void updateMapPaintKnownDefaults() {
896 final String mapPaintStyleEntriesPrefEntry = "mappaint.style.entries";
897 final String url = "url";
898 final String active = "active";
899 final String potlatch2 = "resource://styles/standard/potlatch2.mapcss";
900 final String remotePotlatch2 = "https://josm.openstreetmap.de/josmfile?page=Styles/Potlatch2&zip=1";
901
902 // Remove potlatch2 from the known defaults
903 final List<String> knownDefaults = new ArrayList<>(getList("mappaint.style.known-defaults"));
904 // See #18866: Potlatch 2 internal theme removed in favor of remote theme by Stereo
905 knownDefaults.removeIf(potlatch2::equals);
906
907 // Moved from MapPaintPrefHelper for consistency
908 // XML style is not bundled anymore
909 knownDefaults.removeIf("resource://styles/standard/elemstyles.xml"::equals);
910 putList("mappaint.style.known-defaults", knownDefaults);
911
912 // If the user hasn't set the entries, don't go through the removal process for potlatch 2. There is an issue
913 // where it may clear all paintstyles (done when the user has never touched the style settings).
914 if (!this.settingsMap.containsKey(mapPaintStyleEntriesPrefEntry)) {
915 return;
916 }
917 // Replace potlatch2 in the current style entries, but only if it is enabled. Otherwise, remove it.
918 final List<Map<String, String>> styleEntries = new ArrayList<>(getListOfMaps(mapPaintStyleEntriesPrefEntry));
919 final boolean potlatchEnabled = styleEntries.stream().filter(map -> potlatch2.equals(map.get(url)))
920 .anyMatch(map -> Boolean.parseBoolean(map.get(active)));
921 final boolean remotePotlatch2Present = styleEntries.stream().anyMatch(map -> remotePotlatch2.equals(map.get(url)));
922 // Remove potlatch2 if it is not enabled _or_ the remote potlatch2 version is present
923 styleEntries.removeIf(map -> (!potlatchEnabled || remotePotlatch2Present) && potlatch2.equals(map.get(url)));
924 styleEntries.replaceAll(HashMap::new); // The maps are initially immutable.
925 for (Map<String, String> map : styleEntries) {
926 if (potlatch2.equals(map.get(url))) {
927 map.put(url, remotePotlatch2);
928 }
929 }
930 putListOfMaps(mapPaintStyleEntriesPrefEntry, styleEntries);
931 }
932
933 /**
934 * Enables or not the preferences file auto-save mechanism (save each time a setting is changed).
935 * This behaviour is enabled by default.
936 * @param enable if {@code true}, makes JOSM save preferences file each time a setting is changed
937 * @since 7085
938 */
939 public final void enableSaveOnPut(boolean enable) {
940 synchronized (this) {
941 saveOnPut = enable;
942 }
943 }
944
945 /**
946 * A consumer that can throw an exception
947 * @param <T> The object type to accept
948 * @param <E> The throwable type
949 */
950 @FunctionalInterface
951 private interface ThrowingConsumer<T, E extends Throwable> {
952 /**
953 * Accept an object
954 * @param object The object to accept
955 * @throws E The exception that can be thrown
956 */
957 void accept(T object) throws E;
958 }
959}
Note: See TracBrowser for help on using the repository browser.