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

Last change on this file since 9821 was 9821, checked in by bastiK, 8 years ago

fixed #12522 - Advanced preferences: display default entries consistently

Saves default preference entries to a cache file (cache/default_preferences.xml), so the list of advanced preferences is filled with all known default values consistently from the start and not gradually as you use different features during a session.

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