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

Last change on this file since 16622 was 16622, checked in by simon04, 4 years ago

see #19334 - https://errorprone.info/bugpattern/UnnecessaryLambda

  • Property svn:eol-style set to native
File size: 37.7 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;
6import static org.openstreetmap.josm.tools.Utils.getSystemEnv;
7import static org.openstreetmap.josm.tools.Utils.getSystemProperty;
8
9import java.awt.GraphicsEnvironment;
10import java.io.File;
11import java.io.IOException;
12import java.io.PrintWriter;
13import java.io.Reader;
14import java.io.StringWriter;
15import java.nio.charset.StandardCharsets;
16import java.nio.file.InvalidPathException;
17import java.util.ArrayList;
18import java.util.Arrays;
19import java.util.Collection;
20import java.util.Collections;
21import java.util.HashMap;
22import java.util.HashSet;
23import java.util.List;
24import java.util.Map;
25import java.util.Map.Entry;
26import java.util.Optional;
27import java.util.Set;
28import java.util.SortedMap;
29import java.util.TreeMap;
30import java.util.concurrent.TimeUnit;
31import java.util.stream.Collectors;
32import java.util.stream.Stream;
33
34import javax.swing.JOptionPane;
35import javax.xml.stream.XMLStreamException;
36
37import org.openstreetmap.josm.data.preferences.ColorInfo;
38import org.openstreetmap.josm.data.preferences.JosmBaseDirectories;
39import org.openstreetmap.josm.data.preferences.NamedColorProperty;
40import org.openstreetmap.josm.data.preferences.PreferencesReader;
41import org.openstreetmap.josm.data.preferences.PreferencesWriter;
42import org.openstreetmap.josm.gui.MainApplication;
43import org.openstreetmap.josm.io.NetworkManager;
44import org.openstreetmap.josm.spi.preferences.AbstractPreferences;
45import org.openstreetmap.josm.spi.preferences.Config;
46import org.openstreetmap.josm.spi.preferences.DefaultPreferenceChangeEvent;
47import org.openstreetmap.josm.spi.preferences.IBaseDirectories;
48import org.openstreetmap.josm.spi.preferences.ListSetting;
49import org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent;
50import org.openstreetmap.josm.spi.preferences.Setting;
51import org.openstreetmap.josm.tools.CheckParameterUtil;
52import org.openstreetmap.josm.tools.ListenerList;
53import org.openstreetmap.josm.tools.Logging;
54import org.openstreetmap.josm.tools.PlatformManager;
55import org.openstreetmap.josm.tools.ReflectionUtils;
56import org.openstreetmap.josm.tools.Utils;
57import org.xml.sax.SAXException;
58
59/**
60 * This class holds all preferences for JOSM.
61 *
62 * Other classes can register their beloved properties here. All properties will be
63 * saved upon set-access.
64 *
65 * Each property is a key=setting pair, where key is a String and setting can be one of
66 * 4 types:
67 * string, list, list of lists and list of maps.
68 * In addition, each key has a unique default value that is set when the value is first
69 * accessed using one of the get...() methods. You can use the same preference
70 * key in different parts of the code, but the default value must be the same
71 * everywhere. A default value of null means, the setting has been requested, but
72 * no default value was set. This is used in advanced preferences to present a list
73 * off all possible settings.
74 *
75 * At the moment, you cannot put the empty string for string properties.
76 * put(key, "") means, the property is removed.
77 *
78 * @author imi
79 * @since 74
80 */
81public class Preferences extends AbstractPreferences {
82
83 /** remove if key equals */
84 private static final String[] OBSOLETE_PREF_KEYS = {
85 "remotecontrol.https.enabled", /* remove entry after Dec. 2019 */
86 "remotecontrol.https.port", /* remove entry after Dec. 2019 */
87 "curves.circlearc.angle-separation", // see #19076
88 "update.selected.complete-relation" // see #19124
89 };
90
91 /** remove if key starts with */
92 private static final String[] OBSOLETE_PREF_KEYS_START = {
93 //only remove layer specific prefs
94 "draw.rawgps.layer.wpt.",
95 "draw.rawgps.layer.audiowpt.",
96 "draw.rawgps.lines.force.",
97 "draw.rawgps.lines.alpha-blend.",
98 "draw.rawgps.lines.",
99 "markers.show ", //uses space as separator
100 "marker.makeautomarker.",
101 "clr.layer.",
102
103 //remove both layer specific and global prefs
104 "draw.rawgps.colors",
105 "draw.rawgps.direction",
106 "draw.rawgps.alternatedirection",
107 "draw.rawgps.linewidth",
108 "draw.rawgps.max-line-length.local",
109 "draw.rawgps.max-line-length",
110 "draw.rawgps.large",
111 "draw.rawgps.large.size",
112 "draw.rawgps.hdopcircle",
113 "draw.rawgps.min-arrow-distance",
114 "draw.rawgps.colorTracksTune",
115 "draw.rawgps.colors.dynamic",
116 "draw.rawgps.lines.local",
117 "draw.rawgps.heatmap"
118 };
119
120 /** keep subkey even if it starts with any of {@link #OBSOLETE_PREF_KEYS_START} */
121 private static final List<String> KEEP_PREF_KEYS = Arrays.asList(
122 "draw.rawgps.lines.alpha-blend",
123 "draw.rawgps.lines.arrows",
124 "draw.rawgps.lines.arrows.fast",
125 "draw.rawgps.lines.arrows.min-distance",
126 "draw.rawgps.lines.force",
127 "draw.rawgps.lines.max-length",
128 "draw.rawgps.lines.max-length.local",
129 "draw.rawgps.lines.width"
130 );
131
132 /** rename keys that equal */
133 private static final Map<String, String> UPDATE_PREF_KEYS = getUpdatePrefKeys();
134
135 private static Map<String, String> getUpdatePrefKeys() {
136 HashMap<String, String> m = new HashMap<>();
137 m.put("draw.rawgps.direction", "draw.rawgps.lines.arrows");
138 m.put("draw.rawgps.alternatedirection", "draw.rawgps.lines.arrows.fast");
139 m.put("draw.rawgps.min-arrow-distance", "draw.rawgps.lines.arrows.min-distance");
140 m.put("draw.rawgps.linewidth", "draw.rawgps.lines.width");
141 m.put("draw.rawgps.max-line-length.local", "draw.rawgps.lines.max-length.local");
142 m.put("draw.rawgps.max-line-length", "draw.rawgps.lines.max-length");
143 m.put("draw.rawgps.large", "draw.rawgps.points.large");
144 m.put("draw.rawgps.large.alpha", "draw.rawgps.points.large.alpha");
145 m.put("draw.rawgps.large.size", "draw.rawgps.points.large.size");
146 m.put("draw.rawgps.hdopcircle", "draw.rawgps.points.hdopcircle");
147 m.put("draw.rawgps.layer.wpt.pattern", "draw.rawgps.markers.pattern");
148 m.put("draw.rawgps.layer.audiowpt.pattern", "draw.rawgps.markers.audio.pattern");
149 m.put("draw.rawgps.colors", "draw.rawgps.colormode");
150 m.put("draw.rawgps.colorTracksTune", "draw.rawgps.colormode.velocity.tune");
151 m.put("draw.rawgps.colors.dynamic", "draw.rawgps.colormode.dynamic-range");
152 m.put("draw.rawgps.heatmap.line-extra", "draw.rawgps.colormode.heatmap.line-extra");
153 m.put("draw.rawgps.heatmap.colormap", "draw.rawgps.colormode.heatmap.colormap");
154 m.put("draw.rawgps.heatmap.use-points", "draw.rawgps.colormode.heatmap.use-points");
155 m.put("draw.rawgps.heatmap.gain", "draw.rawgps.colormode.heatmap.gain");
156 m.put("draw.rawgps.heatmap.lower-limit", "draw.rawgps.colormode.heatmap.lower-limit");
157 m.put("draw.rawgps.date-coloring-min-dt", "draw.rawgps.colormode.time.min-distance");
158 return Collections.unmodifiableMap(m);
159 }
160
161 private static final long MAX_AGE_DEFAULT_PREFERENCES = TimeUnit.DAYS.toSeconds(50);
162
163 private final IBaseDirectories dirs;
164 boolean modifiedDefault;
165
166 /**
167 * Determines if preferences file is saved each time a property is changed.
168 */
169 private boolean saveOnPut = true;
170
171 /**
172 * Maps the setting name to the current value of the setting.
173 * The map must not contain null as key or value. The mapped setting objects
174 * must not have a null value.
175 */
176 protected final SortedMap<String, Setting<?>> settingsMap = new TreeMap<>();
177
178 /**
179 * Maps the setting name to the default value of the setting.
180 * The map must not contain null as key or value. The value of the mapped
181 * setting objects can be null.
182 */
183 protected final SortedMap<String, Setting<?>> defaultsMap = new TreeMap<>();
184
185 /**
186 * Indicates whether {@link #init(boolean)} completed successfully.
187 * Used to decide whether to write backup preference file in {@link #save()}
188 */
189 protected boolean initSuccessful;
190
191 private final ListenerList<org.openstreetmap.josm.spi.preferences.PreferenceChangedListener> listeners = ListenerList.create();
192
193 private final HashMap<String, ListenerList<org.openstreetmap.josm.spi.preferences.PreferenceChangedListener>> keyListeners = new HashMap<>();
194
195 private static final Preferences defaultInstance = new Preferences(JosmBaseDirectories.getInstance());
196
197 /**
198 * Preferences classes calling directly the method {@link #putSetting(String, Setting)}.
199 * This collection allows us to exclude them when searching the business class who set a preference.
200 * The found class is used as event source when notifying event listeners.
201 */
202 private static final Collection<Class<?>> preferencesClasses = Arrays.asList(
203 Preferences.class, PreferencesUtils.class, AbstractPreferences.class);
204
205 /**
206 * Constructs a new {@code Preferences}.
207 */
208 public Preferences() {
209 this.dirs = Config.getDirs();
210 }
211
212 /**
213 * Constructs a new {@code Preferences}.
214 *
215 * @param dirs the directories to use for saving the preferences
216 */
217 public Preferences(IBaseDirectories dirs) {
218 this.dirs = dirs;
219 }
220
221 /**
222 * Constructs a new {@code Preferences} from an existing instance.
223 * @param pref existing preferences to copy
224 * @since 12634
225 */
226 public Preferences(Preferences pref) {
227 this(pref.dirs);
228 settingsMap.putAll(pref.settingsMap);
229 defaultsMap.putAll(pref.defaultsMap);
230 }
231
232 /**
233 * Returns the main (default) preferences instance.
234 * @return the main (default) preferences instance
235 * @since 14149
236 */
237 public static Preferences main() {
238 return defaultInstance;
239 }
240
241 /**
242 * Adds a new preferences listener.
243 * @param listener The listener to add
244 * @since 12881
245 */
246 @Override
247 public void addPreferenceChangeListener(org.openstreetmap.josm.spi.preferences.PreferenceChangedListener listener) {
248 if (listener != null) {
249 listeners.addListener(listener);
250 }
251 }
252
253 /**
254 * Removes a preferences listener.
255 * @param listener The listener to remove
256 * @since 12881
257 */
258 @Override
259 public void removePreferenceChangeListener(org.openstreetmap.josm.spi.preferences.PreferenceChangedListener listener) {
260 listeners.removeListener(listener);
261 }
262
263 /**
264 * Adds a listener that only listens to changes in one preference
265 * @param key The preference key to listen to
266 * @param listener The listener to add.
267 * @since 12881
268 */
269 @Override
270 public void addKeyPreferenceChangeListener(String key, org.openstreetmap.josm.spi.preferences.PreferenceChangedListener listener) {
271 listenersForKey(key).addListener(listener);
272 }
273
274 /**
275 * Adds a weak listener that only listens to changes in one preference
276 * @param key The preference key to listen to
277 * @param listener The listener to add.
278 * @since 10824
279 */
280 public void addWeakKeyPreferenceChangeListener(String key, org.openstreetmap.josm.spi.preferences.PreferenceChangedListener listener) {
281 listenersForKey(key).addWeakListener(listener);
282 }
283
284 private ListenerList<org.openstreetmap.josm.spi.preferences.PreferenceChangedListener> listenersForKey(String key) {
285 return keyListeners.computeIfAbsent(key, k -> ListenerList.create());
286 }
287
288 /**
289 * Removes a listener that only listens to changes in one preference
290 * @param key The preference key to listen to
291 * @param listener The listener to add.
292 * @since 12881
293 */
294 @Override
295 public void removeKeyPreferenceChangeListener(String key, org.openstreetmap.josm.spi.preferences.PreferenceChangedListener listener) {
296 Optional.ofNullable(keyListeners.get(key)).orElseThrow(
297 () -> new IllegalArgumentException("There are no listeners registered for " + key))
298 .removeListener(listener);
299 }
300
301 protected void firePreferenceChanged(String key, Setting<?> oldValue, Setting<?> newValue) {
302 final Class<?> source = ReflectionUtils.findCallerClass(preferencesClasses);
303 final PreferenceChangeEvent evt =
304 new DefaultPreferenceChangeEvent(source != null ? source : getClass(), key, oldValue, newValue);
305 listeners.fireEvent(listener -> listener.preferenceChanged(evt));
306
307 ListenerList<org.openstreetmap.josm.spi.preferences.PreferenceChangedListener> forKey = keyListeners.get(key);
308 if (forKey != null) {
309 forKey.fireEvent(listener -> listener.preferenceChanged(evt));
310 }
311 }
312
313 /**
314 * Get the base name of the JOSM directories for preferences, cache and user data.
315 * Default value is "JOSM", unless overridden by system property "josm.dir.name".
316 * @return the base name of the JOSM directories for preferences, cache and user data
317 */
318 public static String getJOSMDirectoryBaseName() {
319 String name = getSystemProperty("josm.dir.name");
320 if (name != null)
321 return name;
322 else
323 return "JOSM";
324 }
325
326 /**
327 * Get the base directories associated with this preference instance.
328 * @return the base directories
329 */
330 public IBaseDirectories getDirs() {
331 return dirs;
332 }
333
334 /**
335 * Returns the user preferences file (preferences.xml).
336 * @return The user preferences file (preferences.xml)
337 */
338 public File getPreferenceFile() {
339 return new File(dirs.getPreferencesDirectory(false), "preferences.xml");
340 }
341
342 /**
343 * Returns the cache file for default preferences.
344 * @return the cache file for default preferences
345 */
346 public File getDefaultsCacheFile() {
347 return new File(dirs.getCacheDirectory(true), "default_preferences.xml");
348 }
349
350 /**
351 * Returns the user plugin directory.
352 * @return The user plugin directory
353 */
354 public File getPluginsDirectory() {
355 return new File(dirs.getUserDataDirectory(false), "plugins");
356 }
357
358 private static void addPossibleResourceDir(Set<String> locations, String s) {
359 if (s != null) {
360 if (!s.endsWith(File.separator)) {
361 s += File.separator;
362 }
363 locations.add(s);
364 }
365 }
366
367 /**
368 * Returns a set of all existing directories where resources could be stored.
369 * @return A set of all existing directories where resources could be stored.
370 */
371 public static Collection<String> getAllPossiblePreferenceDirs() {
372 Set<String> locations = new HashSet<>();
373 addPossibleResourceDir(locations, defaultInstance.dirs.getPreferencesDirectory(false).getPath());
374 addPossibleResourceDir(locations, defaultInstance.dirs.getUserDataDirectory(false).getPath());
375 addPossibleResourceDir(locations, getSystemEnv("JOSM_RESOURCES"));
376 addPossibleResourceDir(locations, getSystemProperty("josm.resources"));
377 locations.addAll(PlatformManager.getPlatform().getPossiblePreferenceDirs());
378 return locations;
379 }
380
381 /**
382 * Get all named colors, including customized and the default ones.
383 * @return a map of all named colors (maps preference key to {@link ColorInfo})
384 */
385 public synchronized Map<String, ColorInfo> getAllNamedColors() {
386 final Map<String, ColorInfo> all = new TreeMap<>();
387 for (final Entry<String, Setting<?>> e : settingsMap.entrySet()) {
388 if (!e.getKey().startsWith(NamedColorProperty.NAMED_COLOR_PREFIX))
389 continue;
390 Utils.instanceOfAndCast(e.getValue(), ListSetting.class)
391 .map(ListSetting::getValue)
392 .map(lst -> ColorInfo.fromPref(lst, false))
393 .ifPresent(info -> all.put(e.getKey(), info));
394 }
395 for (final Entry<String, Setting<?>> e : defaultsMap.entrySet()) {
396 if (!e.getKey().startsWith(NamedColorProperty.NAMED_COLOR_PREFIX))
397 continue;
398 Utils.instanceOfAndCast(e.getValue(), ListSetting.class)
399 .map(ListSetting::getValue)
400 .map(lst -> ColorInfo.fromPref(lst, true))
401 .ifPresent(infoDef -> {
402 ColorInfo info = all.get(e.getKey());
403 if (info == null) {
404 all.put(e.getKey(), infoDef);
405 } else {
406 info.setDefaultValue(infoDef.getDefaultValue());
407 }
408 });
409 }
410 return all;
411 }
412
413 /**
414 * Called after every put. In case of a problem, do nothing but output the error in log.
415 * @throws IOException if any I/O error occurs
416 */
417 public synchronized void save() throws IOException {
418 save(getPreferenceFile(), settingsMap.entrySet().stream().filter(e -> !e.getValue().equals(defaultsMap.get(e.getKey()))), false);
419 }
420
421 /**
422 * Stores the defaults to the defaults file
423 * @throws IOException If the file could not be saved
424 */
425 public synchronized void saveDefaults() throws IOException {
426 save(getDefaultsCacheFile(), defaultsMap.entrySet().stream(), true);
427 }
428
429 protected void save(File prefFile, Stream<Entry<String, Setting<?>>> settings, boolean defaults) throws IOException {
430 if (!defaults) {
431 /* currently unused, but may help to fix configuration issues in future */
432 putInt("josm.version", Version.getInstance().getVersion());
433 }
434
435 File backupFile = new File(prefFile + "_backup");
436
437 // Backup old preferences if there are old preferences
438 if (initSuccessful && prefFile.exists() && prefFile.length() > 0) {
439 Utils.copyFile(prefFile, backupFile);
440 }
441
442 try (PreferencesWriter writer = new PreferencesWriter(
443 new PrintWriter(new File(prefFile + "_tmp"), StandardCharsets.UTF_8.name()), false, defaults)) {
444 writer.write(settings);
445 } catch (SecurityException e) {
446 throw new IOException(e);
447 }
448
449 File tmpFile = new File(prefFile + "_tmp");
450 Utils.copyFile(tmpFile, prefFile);
451 Utils.deleteFile(tmpFile, marktr("Unable to delete temporary file {0}"));
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 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 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 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 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 if (modifiedDefault) {
904 try {
905 saveDefaults();
906 Logging.info(tr("Saved updated default preferences."));
907 } catch (IOException ex) {
908 Logging.log(Logging.LEVEL_WARN, tr("Failed to save default preferences."), ex);
909 }
910 modifiedDefault = false;
911 }
912 }
913
914 /**
915 * Enables or not the preferences file auto-save mechanism (save each time a setting is changed).
916 * This behaviour is enabled by default.
917 * @param enable if {@code true}, makes JOSM save preferences file each time a setting is changed
918 * @since 7085
919 */
920 public final void enableSaveOnPut(boolean enable) {
921 synchronized (this) {
922 saveOnPut = enable;
923 }
924 }
925}
Note: See TracBrowser for help on using the repository browser.