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

Last change on this file was 18908, checked in by taylor.smock, 4 months ago

Fix #23341: Don't set paintstyles to nothing when paintstyles haven't been modified by the user

The original fix (r18907) was a short-term fix that reset paintstyles to the
default if the list was empty. This is not ideal, since people who have
deliberately made their paintstyle list empty now have to clear it every time
JOSM starts.

This patch fixes that by only attempting to remove potlatch2 if the user has
previously modified the painstyle list. The previous patch was necessary to get
most users back to a "known good" state, especially those that have never used
the style settings.

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