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

Last change on this file since 9829 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
RevLine 
[6380]1// License: GPL. For details, see LICENSE file.
[74]2package org.openstreetmap.josm.data;
3
[9296]4import static org.openstreetmap.josm.tools.I18n.marktr;
[1326]5import static org.openstreetmap.josm.tools.I18n.tr;
6
[608]7import java.awt.Color;
[8817]8import java.awt.GraphicsEnvironment;
[5358]9import java.awt.Toolkit;
[172]10import java.io.File;
[1628]11import java.io.FileOutputStream;
[74]12import java.io.IOException;
[1628]13import java.io.OutputStreamWriter;
[74]14import java.io.PrintWriter;
[3938]15import java.io.Reader;
[8344]16import java.io.StringReader;
17import java.io.StringWriter;
[3908]18import java.lang.annotation.Retention;
19import java.lang.annotation.RetentionPolicy;
20import java.lang.reflect.Field;
[7082]21import java.nio.charset.StandardCharsets;
[74]22import java.util.ArrayList;
[172]23import java.util.Collection;
[1121]24import java.util.Collections;
[8344]25import java.util.HashMap;
[7834]26import java.util.HashSet;
[4612]27import java.util.Iterator;
28import java.util.LinkedHashMap;
[172]29import java.util.LinkedList;
[2620]30import java.util.List;
[77]31import java.util.Map;
[4200]32import java.util.Map.Entry;
[7083]33import java.util.Objects;
[5358]34import java.util.ResourceBundle;
[7834]35import java.util.Set;
[74]36import java.util.SortedMap;
37import java.util.TreeMap;
[2618]38import java.util.concurrent.CopyOnWriteArrayList;
[4162]39import java.util.regex.Matcher;
40import java.util.regex.Pattern;
[74]41
[8344]42import javax.json.Json;
[9617]43import javax.json.JsonArray;
44import javax.json.JsonArrayBuilder;
[8344]45import javax.json.JsonObject;
46import javax.json.JsonObjectBuilder;
47import javax.json.JsonReader;
[8541]48import javax.json.JsonString;
[8344]49import javax.json.JsonValue;
50import javax.json.JsonWriter;
[1326]51import javax.swing.JOptionPane;
[4612]52import javax.xml.stream.XMLStreamException;
[1326]53
[4200]54import org.openstreetmap.josm.Main;
[5464]55import org.openstreetmap.josm.data.preferences.ColorProperty;
[9759]56import org.openstreetmap.josm.data.preferences.ListListSetting;
57import org.openstreetmap.josm.data.preferences.ListSetting;
58import org.openstreetmap.josm.data.preferences.MapListSetting;
[9780]59import org.openstreetmap.josm.data.preferences.PreferencesReader;
[9823]60import org.openstreetmap.josm.data.preferences.PreferencesWriter;
[9759]61import org.openstreetmap.josm.data.preferences.Setting;
62import org.openstreetmap.josm.data.preferences.StringSetting;
[8471]63import org.openstreetmap.josm.io.OfflineAccessException;
64import org.openstreetmap.josm.io.OnlineResource;
[6578]65import org.openstreetmap.josm.tools.CheckParameterUtil;
[608]66import org.openstreetmap.josm.tools.ColorHelper;
[9821]67import org.openstreetmap.josm.tools.FilteredCollection;
[6671]68import org.openstreetmap.josm.tools.I18n;
[9755]69import org.openstreetmap.josm.tools.MultiMap;
[9821]70import org.openstreetmap.josm.tools.Predicate;
[3848]71import org.openstreetmap.josm.tools.Utils;
[7315]72import org.xml.sax.SAXException;
[74]73
74/**
75 * This class holds all preferences for JOSM.
[807]76 *
[74]77 * Other classes can register their beloved properties here. All properties will be
78 * saved upon set-access.
[807]79 *
[4635]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.
[3708]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
[4634]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.
[3708]89 *
[4635]90 * At the moment, you cannot put the empty string for string properties.
91 * put(key, "") means, the property is removed.
[3708]92 *
[74]93 * @author imi
[7536]94 * @since 74
[74]95 */
96public class Preferences {
[8846]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
[9821]105 private static final long MAX_AGE_DEFAULT_PREFERENCES = 60 * 60 * 24 * 50; // 50 days (in seconds)
106
[1018]107 /**
[2038]108 * Internal storage for the preference directory.
[1857]109 * Do not access this variable directly!
[7834]110 * @see #getPreferencesDirectory()
[1857]111 */
[8840]112 private File preferencesDir;
[7085]113
[4813]114 /**
115 * Internal storage for the cache directory.
116 */
[8840]117 private File cacheDir;
[7894]118
[7841]119 /**
120 * Internal storage for the user data directory.
121 */
[8840]122 private File userdataDir;
[116]123
[3708]124 /**
[7085]125 * Determines if preferences file is saved each time a property is changed.
126 */
127 private boolean saveOnPut = true;
128
129 /**
[7027]130 * Maps the setting name to the current value of the setting.
[6578]131 * The map must not contain null as key or value. The mapped setting objects
132 * must not have a null value.
[3708]133 */
[7027]134 protected final SortedMap<String, Setting<?>> settingsMap = new TreeMap<>();
135
[6578]136 /**
[7027]137 * Maps the setting name to the default value of the setting.
[6578]138 * The map must not contain null as key or value. The value of the mapped
139 * setting objects can be null.
140 */
[7027]141 protected final SortedMap<String, Setting<?>> defaultsMap = new TreeMap<>();
142
[9821]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
[7027]150 /**
151 * Maps color keys to human readable color name
152 */
[7005]153 protected final SortedMap<String, String> colornames = new TreeMap<>();
[3708]154
[5437]155 /**
[9311]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 /**
[9217]162 * Event triggered when a preference entry value changes.
163 */
[6578]164 public interface PreferenceChangeEvent {
[9217]165 /**
166 * Returns the preference key.
167 * @return the preference key
168 */
[2666]169 String getKey();
[8510]170
[9217]171 /**
172 * Returns the old preference value.
173 * @return the old preference value
174 */
[7027]175 Setting<?> getOldValue();
[8510]176
[9217]177 /**
178 * Returns the new preference value.
179 * @return the new preference value
180 */
[7027]181 Setting<?> getNewValue();
[2618]182 }
183
[9217]184 /**
185 * Listener to preference change events.
186 */
[2666]187 public interface PreferenceChangedListener {
[9217]188 /**
189 * Trigerred when a preference entry value changes.
190 * @param e the preference change event
191 */
[2618]192 void preferenceChanged(PreferenceChangeEvent e);
[1169]193 }
[172]194
[6578]195 private static class DefaultPreferenceChangeEvent implements PreferenceChangeEvent {
[2620]196 private final String key;
[7027]197 private final Setting<?> oldValue;
198 private final Setting<?> newValue;
[2618]199
[8836]200 DefaultPreferenceChangeEvent(String key, Setting<?> oldValue, Setting<?> newValue) {
[2618]201 this.key = key;
202 this.oldValue = oldValue;
203 this.newValue = newValue;
204 }
205
[6084]206 @Override
[2618]207 public String getKey() {
208 return key;
209 }
[7027]210
[6084]211 @Override
[7027]212 public Setting<?> getOldValue() {
[2618]213 return oldValue;
214 }
[7027]215
[6084]216 @Override
[7027]217 public Setting<?> getNewValue() {
[2618]218 return newValue;
219 }
220 }
221
[2666]222 public interface ColorKey {
223 String getColorName();
[8510]224
[2666]225 String getSpecialName();
[8510]226
[5464]227 Color getDefaultValue();
[2666]228 }
229
[7005]230 private final CopyOnWriteArrayList<PreferenceChangedListener> listeners = new CopyOnWriteArrayList<>();
[116]231
[7027]232 /**
233 * Adds a new preferences listener.
234 * @param listener The listener to add
235 */
[2618]236 public void addPreferenceChangeListener(PreferenceChangedListener listener) {
[2655]237 if (listener != null) {
238 listeners.addIfAbsent(listener);
[2618]239 }
240 }
241
[7027]242 /**
243 * Removes a preferences listener.
244 * @param listener The listener to remove
245 */
[2618]246 public void removePreferenceChangeListener(PreferenceChangedListener listener) {
[2655]247 listeners.remove(listener);
[2618]248 }
249
[7027]250 protected void firePreferenceChanged(String key, Setting<?> oldValue, Setting<?> newValue) {
[6578]251 PreferenceChangeEvent evt = new DefaultPreferenceChangeEvent(key, oldValue, newValue);
[2618]252 for (PreferenceChangedListener l : listeners) {
253 l.preferenceChanged(evt);
254 }
255 }
256
[1169]257 /**
[7834]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
[5874]261 */
[7834]262 public File getPreferencesDirectory() {
263 if (preferencesDir != null)
264 return preferencesDir;
[1018]265 String path;
[7841]266 path = System.getProperty("josm.pref");
[1018]267 if (path != null) {
[7834]268 preferencesDir = new File(path).getAbsoluteFile();
[1018]269 } else {
[7841]270 path = System.getProperty("josm.home");
271 if (path != null) {
272 preferencesDir = new File(path).getAbsoluteFile();
273 } else {
274 preferencesDir = Main.platform.getDefaultPrefDirectory();
275 }
[1018]276 }
[7834]277 return preferencesDir;
[1018]278 }
[74]279
[5874]280 /**
[7834]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
[5874]285 */
[7834]286 public File getUserDataDirectory() {
[7841]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;
[7834]302 }
303
304 /**
[9821]305 * Returns the user preferences file (preferences.xml).
[7834]306 * @return The user preferences file (preferences.xml)
307 */
[2053]308 public File getPreferenceFile() {
[7834]309 return new File(getPreferencesDirectory(), "preferences.xml");
[4592]310 }
311
[5874]312 /**
[9821]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.
[5874]322 * @return The user plugin directory
323 */
[2817]324 public File getPluginsDirectory() {
[7834]325 return new File(getUserDataDirectory(), "plugins");
[1169]326 }
[4937]327
[5698]328 /**
329 * Get the directory where cached content of any kind should be stored.
330 *
[9717]331 * If the directory doesn't exist on the file system, it will be created by this method.
[5698]332 *
333 * @return the cache directory
334 */
[4810]335 public File getCacheDirectory() {
[7834]336 if (cacheDir != null)
337 return cacheDir;
[4813]338 String path = System.getProperty("josm.cache");
339 if (path != null) {
[7834]340 cacheDir = new File(path).getAbsoluteFile();
[4813]341 } else {
[7841]342 path = System.getProperty("josm.home");
[4813]343 if (path != null) {
[7841]344 cacheDir = new File(path, "cache");
[4813]345 } else {
[7841]346 path = get("cache.folder", null);
347 if (path != null) {
348 cacheDir = new File(path).getAbsoluteFile();
349 } else {
350 cacheDir = Main.platform.getDefaultCacheDirectory();
351 }
[4813]352 }
353 }
[7834]354 if (!cacheDir.exists() && !cacheDir.mkdirs()) {
355 Main.warn(tr("Failed to create missing cache directory: {0}", cacheDir.getAbsoluteFile()));
[4810]356 JOptionPane.showMessageDialog(
357 Main.parent,
[7834]358 tr("<html>Failed to create missing cache directory: {0}</html>", cacheDir.getAbsoluteFile()),
[4810]359 tr("Error"),
360 JOptionPane.ERROR_MESSAGE
361 );
362 }
[7834]363 return cacheDir;
[4810]364 }
[243]365
[8870]366 private static void addPossibleResourceDir(Set<String> locations, String s) {
[7834]367 if (s != null) {
[1857]368 if (!s.endsWith(File.separator)) {
[7834]369 s += File.separator;
[1857]370 }
[1169]371 locations.add(s);
372 }
[7834]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());
[1857]392 }
[7834]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/");
[1169]398 }
399 return locations;
400 }
[807]401
[3708]402 /**
403 * Get settings value for a certain key.
404 * @param key the identifier for the setting
[9717]405 * @return "" if there is nothing set for the preference key, the corresponding value otherwise. The result is not null.
[3708]406 */
[6986]407 public synchronized String get(final String key) {
[6578]408 String value = get(key, null);
409 return value == null ? "" : value;
[1169]410 }
[807]411
[3708]412 /**
413 * Get settings value for a certain key and provide default a value.
414 * @param key the identifier for the setting
[9717]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
[3708]417 */
[6986]418 public synchronized String get(final String key, final String def) {
[6578]419 return getSetting(key, new StringSetting(def), StringSetting.class).getValue();
[1169]420 }
[807]421
[6986]422 public synchronized Map<String, String> getAllPrefix(final String prefix) {
[8510]423 final Map<String, String> all = new TreeMap<>();
424 for (final Entry<String, Setting<?>> e : settingsMap.entrySet()) {
[6578]425 if (e.getKey().startsWith(prefix) && (e.getValue() instanceof StringSetting)) {
426 all.put(e.getKey(), ((StringSetting) e.getValue()).getValue());
[1857]427 }
[3547]428 }
[1169]429 return all;
430 }
[807]431
[6986]432 public synchronized List<String> getAllPrefixCollectionKeys(final String prefix) {
[7005]433 final List<String> all = new LinkedList<>();
[7027]434 for (Map.Entry<String, Setting<?>> entry : settingsMap.entrySet()) {
[6578]435 if (entry.getKey().startsWith(prefix) && entry.getValue() instanceof ListSetting) {
436 all.add(entry.getKey());
[4895]437 }
438 }
439 return all;
440 }
441
[6986]442 public synchronized Map<String, String> getAllColors() {
[8510]443 final Map<String, String> all = new TreeMap<>();
444 for (final Entry<String, Setting<?>> e : defaultsMap.entrySet()) {
[6578]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 }
[1857]450 }
[3547]451 }
[8510]452 for (final Entry<String, Setting<?>> e : settingsMap.entrySet()) {
[6578]453 if (e.getKey().startsWith("color.") && (e.getValue() instanceof StringSetting)) {
454 all.put(e.getKey().substring(6), ((StringSetting) e.getValue()).getValue());
[1857]455 }
[3547]456 }
[1221]457 return all;
458 }
459
[6986]460 public synchronized boolean getBoolean(final String key) {
[6578]461 String s = get(key, null);
[9074]462 return s != null && Boolean.parseBoolean(s);
[1169]463 }
[75]464
[6986]465 public synchronized boolean getBoolean(final String key, final boolean def) {
[6578]466 return Boolean.parseBoolean(get(key, Boolean.toString(def)));
[1169]467 }
[807]468
[6986]469 public synchronized boolean getBoolean(final String key, final String specName, final boolean def) {
[6578]470 boolean generic = getBoolean(key, def);
[8846]471 String skey = key+'.'+specName;
[7027]472 Setting<?> prop = settingsMap.get(skey);
[6578]473 if (prop instanceof StringSetting)
[8510]474 return Boolean.parseBoolean(((StringSetting) prop).getValue());
[6578]475 else
476 return generic;
[4230]477 }
478
[3708]479 /**
[6578]480 * Set a value for a certain setting.
[3708]481 * @param key the unique identifier for the setting
[9717]482 * @param value the value of the setting. Can be null or "" which both removes the key-value entry.
[9243]483 * @return {@code true}, if something has changed (i.e. value is different than before)
[3708]484 */
[3451]485 public boolean put(final String key, String value) {
[8510]486 if (value != null && value.isEmpty()) {
[6578]487 value = null;
[1169]488 }
[6578]489 return putSetting(key, value == null ? null : new StringSetting(value));
[1169]490 }
[74]491
[3708]492 public boolean put(final String key, final boolean value) {
[1180]493 return put(key, Boolean.toString(value));
[1169]494 }
[74]495
[3708]496 public boolean putInteger(final String key, final Integer value) {
[1180]497 return put(key, Integer.toString(value));
498 }
499
[3708]500 public boolean putDouble(final String key, final Double value) {
[1180]501 return put(key, Double.toString(value));
502 }
503
[3708]504 public boolean putLong(final String key, final Long value) {
[1610]505 return put(key, Long.toString(value));
506 }
507
[1169]508 /**
[7033]509 * Called after every put. In case of a problem, do nothing but output the error in log.
[8926]510 * @throws IOException if any I/O error occurs
[1169]511 */
[2053]512 public void save() throws IOException {
[9821]513 save(getPreferenceFile(),
514 new FilteredCollection<>(settingsMap.entrySet(), NO_DEFAULT_SETTINGS_ENTRY), false);
515 }
[2038]516
[9821]517 public void saveDefaults() throws IOException {
518 save(getDefaultsCacheFile(), defaultsMap.entrySet(), true);
519 }
[2038]520
[9821]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
[4200]530 File backupFile = new File(prefFile + "_backup");
531
[2053]532 // Backup old preferences if there are old preferences
[9311]533 if (prefFile.exists() && prefFile.length() > 0 && initSuccessful) {
[5874]534 Utils.copyFile(prefFile, backupFile);
[2053]535 }
536
[7033]537 try (PrintWriter out = new PrintWriter(new OutputStreamWriter(
[7082]538 new FileOutputStream(prefFile + "_tmp"), StandardCharsets.UTF_8), false)) {
[9823]539 PreferencesWriter writer = new PreferencesWriter(out, false, defaults);
540 writer.write(settings);
[7033]541 }
[2038]542
[2053]543 File tmpFile = new File(prefFile + "_tmp");
[5874]544 Utils.copyFile(tmpFile, prefFile);
[9296]545 Utils.deleteFile(tmpFile, marktr("Unable to delete temporary file {0}"));
[4200]546
547 setCorrectPermissions(prefFile);
548 setCorrectPermissions(backupFile);
[1169]549 }
[74]550
[8870]551 private static void setCorrectPermissions(File file) {
[8357]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 }
[4200]567 }
568
[7315]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 */
[9217]575 protected void load() throws IOException, SAXException, XMLStreamException {
[9780]576 File pref = getPreferenceFile();
577 PreferencesReader.validateXML(pref);
[9823]578 PreferencesReader reader = new PreferencesReader(pref, false);
579 reader.parse();
[6578]580 settingsMap.clear();
[9780]581 settingsMap.putAll(reader.getSettings());
[2358]582 updateSystemProperties();
[9780]583 removeObsolete(reader.getVersion());
[1169]584 }
[116]585
[9805]586 /**
[9821]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);
[9823]598 PreferencesReader reader = new PreferencesReader(def, true);
599 reader.parse();
[9821]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 /**
[9805]610 * Loads preferences from XML reader.
611 * @param in XML reader
612 * @throws XMLStreamException if any XML stream error occurs
[9823]613 * @throws IOException if any I/O error occurs
[9805]614 */
[9823]615 public void fromXML(Reader in) throws XMLStreamException, IOException {
616 PreferencesReader reader = new PreferencesReader(in, false);
617 reader.parse();
[9780]618 settingsMap.clear();
619 settingsMap.putAll(reader.getSettings());
620 }
621
[7315]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) {
[9311]627 initSuccessful = false;
[1326]628 // get the preferences.
[7834]629 File prefDir = getPreferencesDirectory();
[1326]630 if (prefDir.exists()) {
[8510]631 if (!prefDir.isDirectory()) {
[8509]632 Main.warn(tr("Failed to initialize preferences. Preference directory ''{0}'' is not a directory.",
633 prefDir.getAbsoluteFile()));
[2017]634 JOptionPane.showMessageDialog(
[1857]635 Main.parent,
[8509]636 tr("<html>Failed to initialize preferences.<br>Preference directory ''{0}'' is not a directory.</html>",
637 prefDir.getAbsoluteFile()),
[1857]638 tr("Error"),
639 JOptionPane.ERROR_MESSAGE
640 );
[1326]641 return;
642 }
[1857]643 } else {
[8443]644 if (!prefDir.mkdirs()) {
[8509]645 Main.warn(tr("Failed to initialize preferences. Failed to create missing preference directory: {0}",
646 prefDir.getAbsoluteFile()));
[2053]647 JOptionPane.showMessageDialog(
648 Main.parent,
[8509]649 tr("<html>Failed to initialize preferences.<br>Failed to create missing preference directory: {0}</html>",
650 prefDir.getAbsoluteFile()),
[2053]651 tr("Error"),
652 JOptionPane.ERROR_MESSAGE
653 );
654 return;
655 }
[1326]656 }
657
[2053]658 File preferenceFile = getPreferenceFile();
[1326]659 try {
[2053]660 if (!preferenceFile.exists()) {
[6248]661 Main.info(tr("Missing preference file ''{0}''. Creating a default preference file.", preferenceFile.getAbsoluteFile()));
[5761]662 resetToDefault();
663 save();
[2053]664 } else if (reset) {
[9312]665 File backupFile = new File(prefDir, "preferences.xml.bak");
666 Main.platform.rename(preferenceFile, backupFile);
[6248]667 Main.warn(tr("Replacing existing preference file ''{0}'' with default preference file.", preferenceFile.getAbsoluteFile()));
[2053]668 resetToDefault();
669 save();
[1857]670 }
[8510]671 } catch (IOException e) {
[6643]672 Main.error(e);
[2017]673 JOptionPane.showMessageDialog(
[1857]674 Main.parent,
[8509]675 tr("<html>Failed to initialize preferences.<br>Failed to reset preference file to default: {0}</html>",
676 getPreferenceFile().getAbsoluteFile()),
[1857]677 tr("Error"),
678 JOptionPane.ERROR_MESSAGE
679 );
[2053]680 return;
[1326]681 }
[2053]682 try {
683 load();
[9311]684 initSuccessful = true;
[9821]685 } catch (IOException | SAXException | XMLStreamException e) {
[6643]686 Main.error(e);
[8510]687 File backupFile = new File(prefDir, "preferences.xml.bak");
[2053]688 JOptionPane.showMessageDialog(
689 Main.parent,
[8510]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>",
[8509]692 backupFile.getAbsoluteFile()),
[2053]693 tr("Error"),
694 JOptionPane.ERROR_MESSAGE
695 );
[4565]696 Main.platform.rename(preferenceFile, backupFile);
[2053]697 try {
698 resetToDefault();
699 save();
[8510]700 } catch (IOException e1) {
[6643]701 Main.error(e1);
[6248]702 Main.warn(tr("Failed to initialize preferences. Failed to reset preference file to default: {0}", getPreferenceFile()));
[2053]703 }
704 }
[9821]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 }
[1326]718 }
719
[8510]720 public final void resetToDefault() {
[6578]721 settingsMap.clear();
[1169]722 }
[172]723
[1169]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 */
[6986]731 public synchronized Color getColor(String colName, Color def) {
[1221]732 return getColor(colName, null, def);
[1169]733 }
[768]734
[4162]735 /* only for preferences */
[6986]736 public synchronized String getColorName(String o) {
[6310]737 try {
[4162]738 Matcher m = Pattern.compile("mappaint\\.(.+?)\\.(.+)").matcher(o);
[6312]739 if (m.matches()) {
[6671]740 return tr("Paint style {0}: {1}", tr(I18n.escape(m.group(1))), tr(I18n.escape(m.group(2))));
[6312]741 }
[6310]742 } catch (Exception e) {
743 Main.warn(e);
[4162]744 }
[6310]745 try {
[4162]746 Matcher m = Pattern.compile("layer (.+)").matcher(o);
[6312]747 if (m.matches()) {
[6671]748 return tr("Layer: {0}", tr(I18n.escape(m.group(1))));
[6312]749 }
[6310]750 } catch (Exception e) {
751 Main.warn(e);
[4162]752 }
[6671]753 return tr(I18n.escape(colornames.containsKey(o) ? colornames.get(o) : o));
[4162]754 }
755
[6624]756 /**
757 * Returns the color for the given key.
758 * @param key The color key
759 * @return the color
760 */
[2666]761 public Color getColor(ColorKey key) {
[5464]762 return getColor(key.getColorName(), key.getSpecialName(), key.getDefaultValue());
[2666]763 }
764
[1169]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 */
[6986]773 public synchronized Color getColor(String colName, String specName, Color def) {
[5464]774 String colKey = ColorProperty.getColorKey(colName);
[8510]775 if (!colKey.equals(colName)) {
[4162]776 colornames.put(colKey, colName);
[4200]777 }
[1221]778 String colStr = specName != null ? get("color."+specName) : "";
[6624]779 if (colStr.isEmpty()) {
[6774]780 colStr = get("color." + colKey, ColorHelper.color2html(def, true));
[1857]781 }
[6626]782 if (colStr != null && !colStr.isEmpty()) {
783 return ColorHelper.html2color(colStr);
784 } else {
785 return def;
786 }
[1169]787 }
[804]788
[6986]789 public synchronized Color getDefaultColor(String colKey) {
[6578]790 StringSetting col = Utils.cast(defaultsMap.get("color."+colKey), StringSetting.class);
791 String colStr = col == null ? null : col.getValue();
[6087]792 return colStr == null || colStr.isEmpty() ? null : ColorHelper.html2color(colStr);
[1424]793 }
794
[6986]795 public synchronized boolean putColor(String colKey, Color val) {
[6774]796 return put("color."+colKey, val != null ? ColorHelper.color2html(val, true) : null);
[1169]797 }
[807]798
[6986]799 public synchronized int getInteger(String key, int def) {
[6578]800 String v = get(key, Integer.toString(def));
[8510]801 if (v.isEmpty())
[1169]802 return def;
[812]803
[1169]804 try {
805 return Integer.parseInt(v);
[8510]806 } catch (NumberFormatException e) {
[1169]807 // fall out
[8513]808 if (Main.isTraceEnabled()) {
809 Main.trace(e.getMessage());
810 }
[1169]811 }
812 return def;
813 }
[1073]814
[6986]815 public synchronized int getInteger(String key, String specName, int def) {
[8846]816 String v = get(key+'.'+specName);
[8510]817 if (v.isEmpty())
818 v = get(key, Integer.toString(def));
819 if (v.isEmpty())
[4230]820 return def;
821
822 try {
823 return Integer.parseInt(v);
[8510]824 } catch (NumberFormatException e) {
[4230]825 // fall out
[8513]826 if (Main.isTraceEnabled()) {
827 Main.trace(e.getMessage());
828 }
[4230]829 }
830 return def;
831 }
832
[6986]833 public synchronized long getLong(String key, long def) {
[6578]834 String v = get(key, Long.toString(def));
[8510]835 if (null == v)
[1169]836 return def;
[1073]837
[1169]838 try {
839 return Long.parseLong(v);
[8510]840 } catch (NumberFormatException e) {
[1169]841 // fall out
[8513]842 if (Main.isTraceEnabled()) {
843 Main.trace(e.getMessage());
844 }
[1169]845 }
846 return def;
847 }
[812]848
[6986]849 public synchronized double getDouble(String key, double def) {
[6578]850 String v = get(key, Double.toString(def));
[8510]851 if (null == v)
[1169]852 return def;
[938]853
[1169]854 try {
855 return Double.parseDouble(v);
[8510]856 } catch (NumberFormatException e) {
[1169]857 // fall out
[8513]858 if (Main.isTraceEnabled()) {
859 Main.trace(e.getMessage());
860 }
[1169]861 }
862 return def;
863 }
[1062]864
[3709]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.
[9717]869 * @return the corresponding value if the property has been set before, {@code def} otherwise
[3709]870 */
[4612]871 public Collection<String> getCollection(String key, Collection<String> def) {
[6578]872 return getSetting(key, ListSetting.create(def), ListSetting.class).getValue();
[1169]873 }
[3547]874
[3710]875 /**
876 * Get a list of values for a certain key
877 * @param key the identifier for the setting
[9717]878 * @return the corresponding value if the property has been set before, an empty collection otherwise.
[3710]879 */
[4612]880 public Collection<String> getCollection(String key) {
[6578]881 Collection<String> val = getCollection(key, null);
882 return val == null ? Collections.<String>emptyList() : val;
[3710]883 }
884
[6986]885 public synchronized void removeFromCollection(String key, String value) {
[7005]886 List<String> a = new ArrayList<>(getCollection(key, Collections.<String>emptyList()));
[2620]887 a.remove(value);
888 putCollection(key, a);
[1169]889 }
[3547]890
[6578]891 /**
[9717]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.
[6578]894 * @param key the unique identifier for the setting
[9717]895 * @param setting the value of the setting. In case it is null, the key-value entry will be removed.
[9243]896 * @return {@code true}, if something has changed (i.e. value is different than before)
[6578]897 */
[7027]898 public boolean putSetting(final String key, Setting<?> setting) {
[6578]899 CheckParameterUtil.ensureParameterNotNull(key);
900 if (setting != null && setting.getValue() == null)
901 throw new IllegalArgumentException("setting argument must not have null value");
[7027]902 Setting<?> settingOld;
903 Setting<?> settingCopy = null;
[4612]904 synchronized (this) {
[6578]905 if (setting == null) {
906 settingOld = settingsMap.remove(key);
907 if (settingOld == null)
908 return false;
[4612]909 } else {
[6578]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);
[4612]917 }
[7085]918 if (saveOnPut) {
919 try {
920 save();
[8510]921 } catch (IOException e) {
[7085]922 Main.warn(tr("Failed to persist preferences to ''{0}''", getPreferenceFile().getAbsoluteFile()));
923 }
[4920]924 }
[4612]925 }
926 // Call outside of synchronized section in case some listener wait for other thread that wait for preference lock
[6578]927 firePreferenceChanged(key, settingOld, settingCopy);
[4612]928 return true;
[1169]929 }
[3840]930
[7027]931 public synchronized Setting<?> getSetting(String key, Setting<?> def) {
[6578]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
[9717]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.
[6578]941 * @param klass the setting type (same as T)
[9717]942 * @return the corresponding value if the property has been set before, {@code def} otherwise
[6578]943 */
[6792]944 @SuppressWarnings("unchecked")
[7027]945 public synchronized <T extends Setting<?>> T getSetting(String key, T def, Class<T> klass) {
[6578]946 CheckParameterUtil.ensureParameterNotNull(key);
947 CheckParameterUtil.ensureParameterNotNull(def);
[7027]948 Setting<?> oldDef = defaultsMap.get(key);
[9821]949 if (oldDef != null && oldDef.isNew() && oldDef.getValue() != null && def.getValue() != null && !def.equals(oldDef)) {
[6578]950 Main.info("Defaults for " + key + " differ: " + def + " != " + defaultsMap.get(key));
[4612]951 }
[6656]952 if (def.getValue() != null || oldDef == null) {
[9821]953 Setting<?> defCopy = def.copy();
954 defCopy.setTime(System.currentTimeMillis() / 1000);
955 defCopy.setNew(true);
956 defaultsMap.put(key, defCopy);
[6656]957 }
[7027]958 Setting<?> prop = settingsMap.get(key);
[6578]959 if (klass.isInstance(prop)) {
[6792]960 return (T) prop;
[6578]961 } else {
962 return def;
963 }
[4612]964 }
965
[9243]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 */
[6578]972 public boolean putCollection(String key, Collection<String> value) {
973 return putSetting(key, value == null ? null : ListSetting.create(value));
974 }
975
[4371]976 /**
977 * Saves at most {@code maxsize} items of collection {@code val}.
[9243]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)
[4371]982 */
983 public boolean putCollectionBounded(String key, int maxsize, Collection<String> val) {
[7005]984 Collection<String> newCollection = new ArrayList<>(Math.min(maxsize, val.size()));
[4371]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
[3696]994 /**
995 * Used to read a 2-dimensional array of strings from the preference file.
[6578]996 * If not a single entry could be found, <code>def</code> is returned.
[9243]997 * @param key preference key
998 * @param def default array value
999 * @return array value
[4200]1000 */
[6792]1001 @SuppressWarnings({ "unchecked", "rawtypes" })
[6986]1002 public synchronized Collection<Collection<String>> getArray(String key, Collection<Collection<String>> def) {
[6578]1003 ListListSetting val = getSetting(key, ListListSetting.create(def), ListListSetting.class);
[6792]1004 return (Collection) val.getValue();
[4612]1005 }
1006
[4634]1007 public Collection<Collection<String>> getArray(String key) {
[6578]1008 Collection<Collection<String>> res = getArray(key, null);
1009 return res == null ? Collections.<Collection<String>>emptyList() : res;
[4634]1010 }
1011
[9243]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 */
[4612]1018 public boolean putArray(String key, Collection<Collection<String>> value) {
[6578]1019 return putSetting(key, value == null ? null : ListListSetting.create(value));
[4612]1020 }
1021
1022 public Collection<Map<String, String>> getListOfStructs(String key, Collection<Map<String, String>> def) {
[7005]1023 return getSetting(key, new MapListSetting(def == null ? null : new ArrayList<>(def)), MapListSetting.class).getValue();
[3527]1024 }
[2370]1025
[4612]1026 public boolean putListOfStructs(String key, Collection<Map<String, String>> value) {
[7005]1027 return putSetting(key, value == null ? null : new MapListSetting(new ArrayList<>(value)));
[3531]1028 }
1029
[9129]1030 /**
1031 * Annotation used for converting objects to String Maps and vice versa.
[9717]1032 * Indicates that a certain field should be considered in the conversion process. Otherwise it is ignored.
[9217]1033 *
[9129]1034 * @see #serializeStruct(java.lang.Object, java.lang.Class)
[9217]1035 * @see #deserializeStruct(java.util.Map, java.lang.Class)
[9129]1036 */
1037 @Retention(RetentionPolicy.RUNTIME) // keep annotation at runtime
1038 public @interface pref { }
[3908]1039
[2358]1040 /**
[9129]1041 * Annotation used for converting objects to String Maps.
[9717]1042 * Indicates that a certain field should be written to the map, even if the value is the same as the default value.
[9217]1043 *
[9129]1044 * @see #serializeStruct(java.lang.Object, java.lang.Class)
1045 */
1046 @Retention(RetentionPolicy.RUNTIME) // keep annotation at runtime
1047 public @interface writeExplicitly { }
1048
1049 /**
[3908]1050 * Get a list of hashes which are represented by a struct-like class.
[9717]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.
[9246]1053 * @param <T> klass type
[3908]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
[9246]1068 * @param <T> klass type
[9243]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
[3908]1073 */
1074 public <T> List<T> getListOfStructs(String key, Collection<T> def, Class<T> klass) {
[8510]1075 Collection<Map<String, String>> prop =
[4612]1076 getListOfStructs(key, def == null ? null : serializeListOfStructs(def, klass));
1077 if (prop == null)
[7005]1078 return def == null ? null : new ArrayList<>(def);
1079 List<T> lst = new ArrayList<>();
[8510]1080 for (Map<String, String> entries : prop) {
[3908]1081 T struct = deserializeStruct(entries, klass);
1082 lst.add(struct);
1083 }
1084 return lst;
1085 }
1086
1087 /**
[9717]1088 * Convenience method that saves a MapListSetting which is provided as a collection of objects.
[9217]1089 *
[9717]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.
[9217]1092 *
[3908]1093 * Considers only fields that have the @pref annotation.
1094 * In addition it does not write fields with null values. (Thus they are cleared)
[9717]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.
[9217]1097 * @param <T> the class,
[3908]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) {
[4612]1104 return putListOfStructs(key, serializeListOfStructs(val, klass));
[3908]1105 }
1106
[8870]1107 private static <T> Collection<Map<String, String>> serializeListOfStructs(Collection<T> l, Class<T> klass) {
[3908]1108 if (l == null)
1109 return null;
[8510]1110 Collection<Map<String, String>> vals = new ArrayList<>();
[3908]1111 for (T struct : l) {
[4200]1112 if (struct == null) {
[3908]1113 continue;
[4200]1114 }
[3908]1115 vals.add(serializeStruct(struct, klass));
1116 }
1117 return vals;
1118 }
1119
[8344]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();
[8510]1125 for (Object o: map.entrySet()) {
[8344]1126 Entry e = (Entry) o;
[9617]1127 Object evalue = e.getValue();
[9755]1128 object.add(e.getKey().toString(), evalue.toString());
[8344]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()) {
[8541]1142 JsonValue value = e.getValue();
[9755]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();
[9793]1163 for (Object evo: evalue) {
[9755]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();
[9617]1181 if (value instanceof JsonArray) {
[9618]1182 for (JsonString js: ((JsonArray) value).getValuesAs(JsonString.class)) {
[9755]1183 ret.put(e.getKey(), js.getString());
[9617]1184 }
1185 } else if (value instanceof JsonString) {
[8541]1186 // in some cases, when JsonValue.toString() is called, then additional quotation marks are left in value
[8557]1187 ret.put(e.getKey(), ((JsonString) value).getString());
[8541]1188 } else {
1189 ret.put(e.getKey(), e.getValue().toString());
1190 }
[8344]1191 }
1192 }
1193 return ret;
1194 }
1195
[9129]1196 /**
[9717]1197 * Convert an object to a String Map, by using field names and values as map key and value.
[9217]1198 *
[9129]1199 * The field value is converted to a String.
[9217]1200 *
[9129]1201 * Only fields with annotation {@link pref} are taken into account.
[9217]1202 *
[9129]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).
[9717]1205 * The {@link writeExplicitly} annotation overrides this behavior, i.e. the default value will also be written.
[9217]1206 *
[9129]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 */
[8510]1212 public static <T> Map<String, String> serializeStruct(T struct, Class<T> klass) {
[3908]1213 T structPrototype;
1214 try {
1215 structPrototype = klass.newInstance();
[7004]1216 } catch (InstantiationException | IllegalAccessException ex) {
[4450]1217 throw new RuntimeException(ex);
[3908]1218 }
1219
[8510]1220 Map<String, String> hash = new LinkedHashMap<>();
[3908]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) {
[7083]1230 if (f.getAnnotation(writeExplicitly.class) != null || !Objects.equals(fieldValue, defaultFieldValue)) {
[8846]1231 String key = f.getName().replace('_', '-');
[8344]1232 if (fieldValue instanceof Map) {
1233 hash.put(key, mapToJson((Map) fieldValue));
[9755]1234 } else if (fieldValue instanceof MultiMap) {
1235 hash.put(key, multiMapToJson((MultiMap) fieldValue));
[8344]1236 } else {
1237 hash.put(key, fieldValue.toString());
1238 }
[3908]1239 }
1240 }
[7004]1241 } catch (IllegalArgumentException | IllegalAccessException ex) {
[6798]1242 throw new RuntimeException(ex);
[3908]1243 }
1244 }
1245 return hash;
1246 }
1247
[9129]1248 /**
[9717]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.
[9217]1251 *
[9717]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;.
[9217]1254 *
[9129]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 */
[8510]1261 public static <T> T deserializeStruct(Map<String, String> hash, Class<T> klass) {
[3908]1262 T struct = null;
1263 try {
1264 struct = klass.newInstance();
[7004]1265 } catch (InstantiationException | IllegalAccessException ex) {
[6798]1266 throw new RuntimeException(ex);
[3908]1267 }
[8510]1268 for (Entry<String, String> key_value : hash.entrySet()) {
[3908]1269 Object value = null;
1270 Field f;
1271 try {
[8846]1272 f = klass.getDeclaredField(key_value.getKey().replace('-', '_'));
[3908]1273 } catch (NoSuchFieldException ex) {
1274 continue;
1275 } catch (SecurityException ex) {
[6798]1276 throw new RuntimeException(ex);
[3908]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) {
[8390]1283 value = Boolean.valueOf(key_value.getValue());
[3908]1284 } else if (f.getType() == Integer.class || f.getType() == int.class) {
1285 try {
[8390]1286 value = Integer.valueOf(key_value.getValue());
[3908]1287 } catch (NumberFormatException nfe) {
1288 continue;
1289 }
[4452]1290 } else if (f.getType() == Double.class || f.getType() == double.class) {
1291 try {
[8390]1292 value = Double.valueOf(key_value.getValue());
[4452]1293 } catch (NumberFormatException nfe) {
1294 continue;
1295 }
[3908]1296 } else if (f.getType() == String.class) {
[4612]1297 value = key_value.getValue();
[8344]1298 } else if (f.getType().isAssignableFrom(Map.class)) {
1299 value = mapFromJson(key_value.getValue());
[9755]1300 } else if (f.getType().isAssignableFrom(MultiMap.class)) {
1301 value = multiMapFromJson(key_value.getValue());
[8465]1302 } else
[3908]1303 throw new RuntimeException("unsupported preference primitive type");
[3938]1304
[3908]1305 try {
1306 f.set(struct, value);
1307 } catch (IllegalArgumentException ex) {
[6798]1308 throw new AssertionError(ex);
[3908]1309 } catch (IllegalAccessException ex) {
[6798]1310 throw new RuntimeException(ex);
[3908]1311 }
1312 }
1313 return struct;
1314 }
1315
[7027]1316 public Map<String, Setting<?>> getAllSettings() {
[7005]1317 return new TreeMap<>(settingsMap);
[4634]1318 }
1319
[7027]1320 public Map<String, Setting<?>> getAllDefaults() {
[7005]1321 return new TreeMap<>(defaultsMap);
[4634]1322 }
1323
[3908]1324 /**
[2358]1325 * Updates system properties with the current values in the preferences.
[2370]1326 *
[2358]1327 */
1328 public void updateSystemProperties() {
[9717]1329 if ("true".equals(get("prefer.ipv6", "auto")) && !"true".equals(Utils.updateSystemProperty("java.net.preferIPv6Addresses", "true"))) {
[5892]1330 // never set this to false, only true!
[9717]1331 Main.info(tr("Try enabling IPv6 network, prefering IPv6 over IPv4 (only works on early startup)."));
[5892]1332 }
[7894]1333 Utils.updateSystemProperty("http.agent", Version.getInstance().getAgentString());
1334 Utils.updateSystemProperty("user.language", get("language"));
[9717]1335 // Workaround to fix a Java bug. This ugly hack comes from Sun bug database: https://bugs.openjdk.java.net/browse/JDK-6292739
[8816]1336 // Force AWT toolkit to update its internal preferences (fix #6345).
[8817]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
[8924]1345 // InternalError catch to remove when https://bugs.openjdk.java.net/browse/JDK-8136804 is resolved
[8817]1346 if (Main.isTraceEnabled()) {
1347 Main.trace(e.getMessage());
1348 }
[8513]1349 }
[5358]1350 }
[9218]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)) {
[7894]1355 Utils.updateSystemProperty("jsse.enableSNIExtension", "false");
[6950]1356 }
[9717]1357 // Workaround to fix another Java bug - The bug seems to have been fixed in Java 8, to remove during transition
[5977]1358 // Force Java 7 to use old sorting algorithm of Arrays.sort (fix #8712).
[6617]1359 // See Oracle bug database: https://bugs.openjdk.java.net/browse/JDK-7075600
1360 // and https://bugs.openjdk.java.net/browse/JDK-6923200
[6780]1361 if (getBoolean("jdk.Arrays.useLegacyMergeSort", !Version.getInstance().isLocalBuild())) {
[7894]1362 Utils.updateSystemProperty("java.util.Arrays.useLegacyMergeSort", "true");
[5977]1363 }
[1169]1364 }
[6069]1365
[6851]1366 /**
[6780]1367 * Replies the collection of plugin site URLs from where plugin lists can be downloaded.
1368 * @return the collection of plugin site URLs
[8471]1369 * @see #getOnlinePluginSites
[2817]1370 */
1371 public Collection<String> getPluginSites() {
[7655]1372 return getCollection("pluginmanager.sites", Collections.singleton(Main.getJOSMWebsite()+"/pluginicons%<?plugins=>"));
[2817]1373 }
1374
1375 /**
[8471]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 /**
[2817]1394 * Sets the collection of plugin site URLs.
[3194]1395 *
[2817]1396 * @param sites the site URLs
1397 */
1398 public void setPluginSites(Collection<String> sites) {
1399 putCollection("pluginmanager.sites", sites);
1400 }
[3938]1401
[9805]1402 /**
1403 * Returns XML describing these preferences.
1404 * @param nopass if password must be excluded
1405 * @return XML
1406 */
[4612]1407 public String toXML(boolean nopass) {
[9821]1408 return toXML(settingsMap.entrySet(), nopass, false);
[9780]1409 }
1410
[9805]1411 /**
1412 * Returns XML describing the given preferences.
1413 * @param settings preferences settings
1414 * @param nopass if password must be excluded
[9822]1415 * @param defaults true, if default values are converted to XML, false for
1416 * regular preferences
[9805]1417 * @return XML
1418 */
[9821]1419 public String toXML(Collection<Entry<String, Setting<?>>> settings, boolean nopass, boolean defaults) {
[9829]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 }
[3938]1431 }
[4932]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.
[9793]1437 * @param loadedVersion JOSM version when the preferences file was written
[4932]1438 */
[9780]1439 private void removeObsolete(int loadedVersion) {
[9119]1440 // drop this block march 2016
1441 // update old style JOSM server links to use zip now, see #10581, #12189
[8100]1442 // actually also cache and mirror entries should be cleared
[9216]1443 if (loadedVersion < 9216) {
[8510]1444 for (String key: new String[]{"mappaint.style.entries", "taggingpreset.entries"}) {
[8100]1445 Collection<Map<String, String>> data = getListOfStructs(key, (Collection<Map<String, String>>) null);
1446 if (data != null) {
[9070]1447 List<Map<String, String>> newlist = new ArrayList<>();
[8100]1448 boolean modified = false;
[8510]1449 for (Map<String, String> map : data) {
[9070]1450 Map<String, String> newmap = new LinkedHashMap<>();
[8100]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 }
[9120]1458 if ("url".equals(mkey) && val.contains("http://josm.openstreetmap.de/josmfile")) {
1459 val = val.replace("http://", "https://");
1460 modified = true;
1461 }
[8100]1462 newmap.put(mkey, val);
1463 }
1464 newlist.add(newmap);
1465 }
1466 if (modified) {
1467 putListOfStructs(key, newlist);
1468 }
1469 }
[6076]1470 }
1471 }
[9715]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;
[9717]1478 for (Map<String, String> map: ((MapListSetting) setting).getValue()) {
[9715]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 }
[6076]1497
[8846]1498 for (String key : OBSOLETE_PREF_KEYS) {
[6578]1499 if (settingsMap.containsKey(key)) {
1500 settingsMap.remove(key);
[6248]1501 Main.info(tr("Preference setting {0} has been removed since it is no longer used.", key));
1502 }
[4932]1503 }
1504 }
[5114]1505
[7085]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 }
[74]1517}
Note: See TracBrowser for help on using the repository browser.