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

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

fix many checkstyle violations

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