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