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

Last change on this file since 9853 was 9829, checked in by Don-vip, 8 years ago

fix warnings, add unit test

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