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

Last change on this file since 8415 was 8395, checked in by Don-vip, 9 years ago

convention - An open curly brace should be located at the end of a line

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