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

Last change on this file since 4541 was 4512, checked in by stoecker, 13 years ago

introduce expert mode, make dynamic toolbar buttons default

  • Property svn:eol-style set to native
File size: 39.1 KB
Line 
1// License: GPL. Copyright 2007 by Immanuel Scholz and others
2package org.openstreetmap.josm.data;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.Color;
7import java.io.BufferedReader;
8import java.io.File;
9import java.io.FileInputStream;
10import java.io.FileOutputStream;
11import java.io.IOException;
12import java.io.InputStreamReader;
13import java.io.OutputStreamWriter;
14import java.io.PrintWriter;
15import java.io.Reader;
16import java.lang.annotation.Retention;
17import java.lang.annotation.RetentionPolicy;
18import java.lang.reflect.Field;
19import java.nio.channels.FileChannel;
20import java.util.ArrayList;
21import java.util.Arrays;
22import java.util.Collection;
23import java.util.Collections;
24import java.util.LinkedList;
25import java.util.List;
26import java.util.Map;
27import java.util.Map.Entry;
28import java.util.Properties;
29import java.util.SortedMap;
30import java.util.TreeMap;
31import java.util.concurrent.CopyOnWriteArrayList;
32import java.util.regex.Matcher;
33import java.util.regex.Pattern;
34
35import javax.swing.JOptionPane;
36import javax.swing.UIManager;
37
38import org.openstreetmap.josm.Main;
39import org.openstreetmap.josm.io.XmlWriter;
40import org.openstreetmap.josm.tools.ColorHelper;
41import org.openstreetmap.josm.tools.Utils;
42import org.openstreetmap.josm.tools.XmlObjectParser;
43import org.xml.sax.SAXException;
44
45/**
46 * This class holds all preferences for JOSM.
47 *
48 * Other classes can register their beloved properties here. All properties will be
49 * saved upon set-access.
50 *
51 * Each property is a simple key=value pair of Strings.
52 * In addition, each key has a unique default value that is set when the value is first
53 * accessed using one of the get...() methods. You can use the same preference
54 * key in different parts of the code, but the default value must be the same
55 * everywhere. null is a legitimate default value.
56 *
57 * At the moment, there is no such thing as an empty value.
58 * If you put "" or null as value, the property is removed.
59 *
60 * @author imi
61 */
62public class Preferences {
63 /**
64 * Internal storage for the preference directory.
65 * Do not access this variable directly!
66 * @see #getPreferencesDirFile()
67 */
68 private File preferencesDirFile = null;
69
70 /**
71 * Map the property name to the property object. Does not contain null or "" values.
72 */
73 protected final SortedMap<String, String> properties = new TreeMap<String, String>();
74 protected final SortedMap<String, String> defaults = new TreeMap<String, String>();
75 protected final SortedMap<String, String> colornames = new TreeMap<String, String>();
76
77 public interface PreferenceChangeEvent{
78 String getKey();
79 String getOldValue();
80 String getNewValue();
81 }
82
83 public interface PreferenceChangedListener {
84 void preferenceChanged(PreferenceChangeEvent e);
85 }
86
87 private static class DefaultPreferenceChangeEvent implements PreferenceChangeEvent {
88 private final String key;
89 private final String oldValue;
90 private final String newValue;
91
92 public DefaultPreferenceChangeEvent(String key, String oldValue, String newValue) {
93 this.key = key;
94 this.oldValue = oldValue;
95 this.newValue = newValue;
96 }
97
98 public String getKey() {
99 return key;
100 }
101 public String getOldValue() {
102 return oldValue;
103 }
104 public String getNewValue() {
105 return newValue;
106 }
107 }
108
109 public interface ColorKey {
110 String getColorName();
111 String getSpecialName();
112 Color getDefault();
113 }
114
115 private final CopyOnWriteArrayList<PreferenceChangedListener> listeners = new CopyOnWriteArrayList<PreferenceChangedListener>();
116
117 public void addPreferenceChangeListener(PreferenceChangedListener listener) {
118 if (listener != null) {
119 listeners.addIfAbsent(listener);
120 }
121 }
122
123 public void removePreferenceChangeListener(PreferenceChangedListener listener) {
124 listeners.remove(listener);
125 }
126
127 protected void firePreferenceChanged(String key, String oldValue, String newValue) {
128 PreferenceChangeEvent evt = new DefaultPreferenceChangeEvent(key, oldValue, newValue);
129 for (PreferenceChangedListener l : listeners) {
130 l.preferenceChanged(evt);
131 }
132 }
133
134 /**
135 * Return the location of the user defined preferences file
136 */
137 public String getPreferencesDir() {
138 final String path = getPreferencesDirFile().getPath();
139 if (path.endsWith(File.separator))
140 return path;
141 return path + File.separator;
142 }
143
144 public File getPreferencesDirFile() {
145 if (preferencesDirFile != null)
146 return preferencesDirFile;
147 String path;
148 path = System.getProperty("josm.home");
149 if (path != null) {
150 preferencesDirFile = new File(path);
151 } else {
152 path = System.getenv("APPDATA");
153 if (path != null) {
154 preferencesDirFile = new File(path, "JOSM");
155 } else {
156 preferencesDirFile = new File(System.getProperty("user.home"), ".josm");
157 }
158 }
159 return preferencesDirFile;
160 }
161
162 public File getPreferenceFile() {
163 return new File(getPreferencesDirFile(), "preferences");
164 }
165
166 public File getPluginsDirectory() {
167 return new File(getPreferencesDirFile(), "plugins");
168 }
169
170 /**
171 * @return A list of all existing directories where resources could be stored.
172 */
173 public Collection<String> getAllPossiblePreferenceDirs() {
174 LinkedList<String> locations = new LinkedList<String>();
175 locations.add(Main.pref.getPreferencesDir());
176 String s;
177 if ((s = System.getenv("JOSM_RESOURCES")) != null) {
178 if (!s.endsWith(File.separator)) {
179 s = s + File.separator;
180 }
181 locations.add(s);
182 }
183 if ((s = System.getProperty("josm.resources")) != null) {
184 if (!s.endsWith(File.separator)) {
185 s = s + File.separator;
186 }
187 locations.add(s);
188 }
189 String appdata = System.getenv("APPDATA");
190 if (System.getenv("ALLUSERSPROFILE") != null && appdata != null
191 && appdata.lastIndexOf(File.separator) != -1) {
192 appdata = appdata.substring(appdata.lastIndexOf(File.separator));
193 locations.add(new File(new File(System.getenv("ALLUSERSPROFILE"),
194 appdata), "JOSM").getPath());
195 }
196 locations.add("/usr/local/share/josm/");
197 locations.add("/usr/local/lib/josm/");
198 locations.add("/usr/share/josm/");
199 locations.add("/usr/lib/josm/");
200 return locations;
201 }
202
203 synchronized public boolean hasKey(final String key) {
204 return properties.containsKey(key);
205 }
206
207 /**
208 * Get settings value for a certain key.
209 * @param key the identifier for the setting
210 * @return "" if there is nothing set for the preference key,
211 * the corresponding value otherwise. The result is not null.
212 */
213 synchronized public String get(final String key) {
214 putDefault(key, null);
215 if (!properties.containsKey(key))
216 return "";
217 return properties.get(key);
218 }
219
220 /**
221 * Get settings value for a certain key and provide default a value.
222 * @param key the identifier for the setting
223 * @param def the default value. For each call of get() with a given key, the
224 * default value must be the same.
225 * @return the corresponding value if the property has been set before,
226 * def otherwise
227 */
228 synchronized public String get(final String key, final String def) {
229 putDefault(key, def);
230 final String prop = properties.get(key);
231 if (prop == null || prop.equals(""))
232 return def;
233 return prop;
234 }
235
236 synchronized public Map<String, String> getAllPrefix(final String prefix) {
237 final Map<String,String> all = new TreeMap<String,String>();
238 for (final Entry<String,String> e : properties.entrySet()) {
239 if (e.getKey().startsWith(prefix)) {
240 all.put(e.getKey(), e.getValue());
241 }
242 }
243 return all;
244 }
245
246 synchronized private Map<String, String> getAllPrefixDefault(final String prefix) {
247 final Map<String,String> all = new TreeMap<String,String>();
248 for (final Entry<String,String> e : defaults.entrySet()) {
249 if (e.getKey().startsWith(prefix)) {
250 all.put(e.getKey(), e.getValue());
251 }
252 }
253 return all;
254 }
255
256 synchronized public TreeMap<String, String> getAllColors() {
257 final TreeMap<String,String> all = new TreeMap<String,String>();
258 for (final Entry<String,String> e : defaults.entrySet()) {
259 if (e.getKey().startsWith("color.") && e.getValue() != null) {
260 all.put(e.getKey().substring(6), e.getValue());
261 }
262 }
263 for (final Entry<String,String> e : properties.entrySet()) {
264 if (e.getKey().startsWith("color.")) {
265 all.put(e.getKey().substring(6), e.getValue());
266 }
267 }
268 return all;
269 }
270
271 synchronized public Map<String, String> getDefaults() {
272 return defaults;
273 }
274
275 synchronized public void putDefault(final String key, final String def) {
276 if(!defaults.containsKey(key) || defaults.get(key) == null) {
277 defaults.put(key, def);
278 } else if(def != null && !defaults.get(key).equals(def)) {
279 System.out.println("Defaults for " + key + " differ: " + def + " != " + defaults.get(key));
280 }
281 }
282
283 synchronized public boolean getBoolean(final String key) {
284 putDefault(key, null);
285 return properties.containsKey(key) ? Boolean.parseBoolean(properties.get(key)) : false;
286 }
287
288 synchronized public boolean getBoolean(final String key, final boolean def) {
289 putDefault(key, Boolean.toString(def));
290 return properties.containsKey(key) ? Boolean.parseBoolean(properties.get(key)) : def;
291 }
292
293 synchronized public boolean getBoolean(final String key, final String specName, final boolean def) {
294 putDefault(key, Boolean.toString(def));
295 String skey = key+"."+specName;
296 if(properties.containsKey(skey))
297 return Boolean.parseBoolean(properties.get(skey));
298 return properties.containsKey(key) ? Boolean.parseBoolean(properties.get(key)) : def;
299 }
300
301 /**
302 * Set a value for a certain setting. The changed setting is saved
303 * to the preference file immediately. Due to caching mechanisms on modern
304 * operating systems and hardware, this shouldn't be a performance problem.
305 * @param key the unique identifier for the setting
306 * @param value the value of the setting. Can be null or "" wich both removes
307 * the key-value entry.
308 * @return if true, something has changed (i.e. value is different than before)
309 */
310 public boolean put(final String key, String value) {
311 boolean changed = false;
312 String oldValue = null;
313
314 synchronized (this) {
315 oldValue = properties.get(key);
316 if(value != null && value.length() == 0) {
317 value = null;
318 }
319 // value is the same as before - no need to save anything
320 boolean equalValue = oldValue != null && oldValue.equals(value);
321 // The setting was previously unset and we are supposed to put a
322 // value that equals the default value. This is not necessary because
323 // the default value is the same throughout josm. In addition we like
324 // to have the possibility to change the default value from version
325 // to version, which would not work if we wrote it to the preference file.
326 boolean unsetIsDefault = oldValue == null && (value == null || value.equals(defaults.get(key)));
327
328 if (!(equalValue || unsetIsDefault)) {
329 if (value == null) {
330 properties.remove(key);
331 } else {
332 properties.put(key, value);
333 }
334 try {
335 save();
336 } catch(IOException e){
337 System.out.println(tr("Warning: failed to persist preferences to ''{0}''", getPreferenceFile().getAbsoluteFile()));
338 }
339 changed = true;
340 }
341 }
342 if (changed) {
343 // Call outside of synchronized section in case some listener wait for other thread that wait for preference lock
344 firePreferenceChanged(key, oldValue, value);
345 }
346 return changed;
347 }
348
349 public boolean put(final String key, final boolean value) {
350 return put(key, Boolean.toString(value));
351 }
352
353 public boolean putInteger(final String key, final Integer value) {
354 return put(key, Integer.toString(value));
355 }
356
357 public boolean putDouble(final String key, final Double value) {
358 return put(key, Double.toString(value));
359 }
360
361 public boolean putLong(final String key, final Long value) {
362 return put(key, Long.toString(value));
363 }
364
365 /**
366 * Called after every put. In case of a problem, do nothing but output the error
367 * in log.
368 */
369 public void save() throws IOException {
370 /* currently unused, but may help to fix configuration issues in future */
371 putInteger("josm.version", Version.getInstance().getVersion());
372
373 updateSystemProperties();
374 if(Main.applet)
375 return;
376
377 File prefFile = getPreferenceFile();
378 File backupFile = new File(prefFile + "_backup");
379
380 // Backup old preferences if there are old preferences
381 if(prefFile.exists()) {
382 copyFile(prefFile, backupFile);
383 }
384
385 final PrintWriter out = new PrintWriter(new OutputStreamWriter(
386 new FileOutputStream(prefFile + "_tmp"), "utf-8"), false);
387 for (final Entry<String, String> e : properties.entrySet()) {
388 String s = defaults.get(e.getKey());
389 /* don't save default values */
390 if(s == null || !s.equals(e.getValue())) {
391 out.println(e.getKey() + "=" + e.getValue());
392 }
393 }
394 out.close();
395
396 File tmpFile = new File(prefFile + "_tmp");
397 copyFile(tmpFile, prefFile);
398 tmpFile.delete();
399
400 setCorrectPermissions(prefFile);
401 setCorrectPermissions(backupFile);
402 }
403
404
405 private void setCorrectPermissions(File file) {
406 file.setReadable(false, false);
407 file.setWritable(false, false);
408 file.setExecutable(false, false);
409 file.setReadable(true, true);
410 file.setWritable(true, true);
411 }
412
413 /**
414 * Simple file copy function that will overwrite the target file
415 * Taken from http://www.rgagnon.com/javadetails/java-0064.html (CC-NC-BY-SA)
416 * @param in
417 * @param out
418 * @throws IOException
419 */
420 public static void copyFile(File in, File out) throws IOException {
421 FileChannel inChannel = new FileInputStream(in).getChannel();
422 FileChannel outChannel = new FileOutputStream(out).getChannel();
423 try {
424 inChannel.transferTo(0, inChannel.size(),
425 outChannel);
426 }
427 catch (IOException e) {
428 throw e;
429 }
430 finally {
431 if (inChannel != null) {
432 inChannel.close();
433 }
434 if (outChannel != null) {
435 outChannel.close();
436 }
437 }
438 }
439
440 public void load() throws IOException {
441 properties.clear();
442 if(!Main.applet) {
443 final BufferedReader in = new BufferedReader(new InputStreamReader(
444 new FileInputStream(getPreferencesDir()+"preferences"), "utf-8"));
445 int lineNumber = 0;
446 ArrayList<Integer> errLines = new ArrayList<Integer>();
447 for (String line = in.readLine(); line != null; line = in.readLine(), lineNumber++) {
448 final int i = line.indexOf('=');
449 if (i == -1 || i == 0) {
450 errLines.add(lineNumber);
451 continue;
452 }
453 String key = line.substring(0,i);
454 String value = line.substring(i+1);
455 if (!value.isEmpty()) {
456 properties.put(key, value);
457 }
458 }
459 if (!errLines.isEmpty())
460 throw new IOException(tr("Malformed config file at lines {0}", errLines));
461 }
462 updateSystemProperties();
463 /* FIXME: TODO: remove special version check end of 2012 */
464 if(!properties.containsKey("expert")) {
465 try {
466 String v = get("josm.version");
467 if(v.isEmpty() || Integer.parseInt(v) <= 4511)
468 properties.put("expert", "true");
469 } catch(Exception e) {
470 properties.put("expert", "true");
471 }
472 }
473 }
474
475 public void init(boolean reset){
476 if(Main.applet)
477 return;
478 // get the preferences.
479 File prefDir = getPreferencesDirFile();
480 if (prefDir.exists()) {
481 if(!prefDir.isDirectory()) {
482 System.err.println(tr("Warning: Failed to initialize preferences. Preference directory ''{0}'' is not a directory.", prefDir.getAbsoluteFile()));
483 JOptionPane.showMessageDialog(
484 Main.parent,
485 tr("<html>Failed to initialize preferences.<br>Preference directory ''{0}'' is not a directory.</html>", prefDir.getAbsoluteFile()),
486 tr("Error"),
487 JOptionPane.ERROR_MESSAGE
488 );
489 return;
490 }
491 } else {
492 if (! prefDir.mkdirs()) {
493 System.err.println(tr("Warning: Failed to initialize preferences. Failed to create missing preference directory: {0}", prefDir.getAbsoluteFile()));
494 JOptionPane.showMessageDialog(
495 Main.parent,
496 tr("<html>Failed to initialize preferences.<br>Failed to create missing preference directory: {0}</html>",prefDir.getAbsoluteFile()),
497 tr("Error"),
498 JOptionPane.ERROR_MESSAGE
499 );
500 return;
501 }
502 }
503
504 File preferenceFile = getPreferenceFile();
505 try {
506 if (!preferenceFile.exists()) {
507 System.out.println(tr("Warning: Missing preference file ''{0}''. Creating a default preference file.", preferenceFile.getAbsoluteFile()));
508 resetToDefault();
509 save();
510 } else if (reset) {
511 System.out.println(tr("Warning: Replacing existing preference file ''{0}'' with default preference file.", preferenceFile.getAbsoluteFile()));
512 resetToDefault();
513 save();
514 }
515 } catch(IOException e) {
516 e.printStackTrace();
517 JOptionPane.showMessageDialog(
518 Main.parent,
519 tr("<html>Failed to initialize preferences.<br>Failed to reset preference file to default: {0}</html>",getPreferenceFile().getAbsoluteFile()),
520 tr("Error"),
521 JOptionPane.ERROR_MESSAGE
522 );
523 return;
524 }
525 try {
526 load();
527 } catch (IOException e) {
528 e.printStackTrace();
529 File backupFile = new File(prefDir,"preferences.bak");
530 JOptionPane.showMessageDialog(
531 Main.parent,
532 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()),
533 tr("Error"),
534 JOptionPane.ERROR_MESSAGE
535 );
536 preferenceFile.renameTo(backupFile);
537 try {
538 resetToDefault();
539 save();
540 } catch(IOException e1) {
541 e1.printStackTrace();
542 System.err.println(tr("Warning: Failed to initialize preferences. Failed to reset preference file to default: {0}", getPreferenceFile()));
543 }
544 }
545 }
546
547 public final void resetToDefault(){
548 properties.clear();
549 }
550
551 /**
552 * Convenience method for accessing colour preferences.
553 *
554 * @param colName name of the colour
555 * @param def default value
556 * @return a Color object for the configured colour, or the default value if none configured.
557 */
558 synchronized public Color getColor(String colName, Color def) {
559 return getColor(colName, null, def);
560 }
561
562 synchronized public Color getUIColor(String colName) {
563 return UIManager.getColor(colName);
564 }
565
566 /* only for preferences */
567 synchronized public String getColorName(String o) {
568 try
569 {
570 Matcher m = Pattern.compile("mappaint\\.(.+?)\\.(.+)").matcher(o);
571 m.matches();
572 return tr("Paint style {0}: {1}", tr(m.group(1)), tr(m.group(2)));
573 }
574 catch (Exception e) {}
575 try
576 {
577 Matcher m = Pattern.compile("layer (.+)").matcher(o);
578 m.matches();
579 return tr("Layer: {0}", tr(m.group(1)));
580 }
581 catch (Exception e) {}
582 return tr(colornames.containsKey(o) ? colornames.get(o) : o);
583 }
584
585 public Color getColor(ColorKey key) {
586 return getColor(key.getColorName(), key.getSpecialName(), key.getDefault());
587 }
588
589 /**
590 * Convenience method for accessing colour preferences.
591 *
592 * @param colName name of the colour
593 * @param specName name of the special colour settings
594 * @param def default value
595 * @return a Color object for the configured colour, or the default value if none configured.
596 */
597 synchronized public Color getColor(String colName, String specName, Color def) {
598 String colKey = colName.toLowerCase().replaceAll("[^a-z0-9]+",".");
599 if(!colKey.equals(colName)) {
600 colornames.put(colKey, colName);
601 }
602 putDefault("color."+colKey, ColorHelper.color2html(def));
603 String colStr = specName != null ? get("color."+specName) : "";
604 if(colStr.equals("")) {
605 colStr = get("color."+colKey);
606 }
607 return colStr.equals("") ? def : ColorHelper.html2color(colStr);
608 }
609
610 synchronized public Color getDefaultColor(String colName) {
611 String colStr = defaults.get("color."+colName);
612 return colStr == null || "".equals(colStr) ? null : ColorHelper.html2color(colStr);
613 }
614
615 synchronized public boolean putColor(String colName, Color val) {
616 return put("color."+colName, val != null ? ColorHelper.color2html(val) : null);
617 }
618
619 synchronized public int getInteger(String key, int def) {
620 putDefault(key, Integer.toString(def));
621 String v = get(key);
622 if(v.isEmpty())
623 return def;
624
625 try {
626 return Integer.parseInt(v);
627 } catch(NumberFormatException e) {
628 // fall out
629 }
630 return def;
631 }
632
633 synchronized public int getInteger(String key, String specName, int def) {
634 putDefault(key, Integer.toString(def));
635 String v = get(key+"."+specName);
636 if(v.isEmpty())
637 v = get(key);
638 if(v.isEmpty())
639 return def;
640
641 try {
642 return Integer.parseInt(v);
643 } catch(NumberFormatException e) {
644 // fall out
645 }
646 return def;
647 }
648
649 synchronized public long getLong(String key, long def) {
650 putDefault(key, Long.toString(def));
651 String v = get(key);
652 if(null == v)
653 return def;
654
655 try {
656 return Long.parseLong(v);
657 } catch(NumberFormatException e) {
658 // fall out
659 }
660 return def;
661 }
662
663 synchronized public double getDouble(String key, double def) {
664 putDefault(key, Double.toString(def));
665 String v = get(key);
666 if(null == v)
667 return def;
668
669 try {
670 return Double.parseDouble(v);
671 } catch(NumberFormatException e) {
672 // fall out
673 }
674 return def;
675 }
676
677 synchronized public double getDouble(String key, String def) {
678 putDefault(key, def);
679 String v = get(key);
680 if(v != null && v.length() != 0) {
681 try { return Double.parseDouble(v); } catch(NumberFormatException e) {}
682 }
683 try { return Double.parseDouble(def); } catch(NumberFormatException e) {}
684 return 0.0;
685 }
686
687 synchronized public String getCollectionAsString(final String key) {
688 String s = get(key);
689 if(s != null && s.length() != 0) {
690 s = s.replaceAll("\u001e",",");
691 }
692 return s;
693 }
694
695 public boolean isCollection(String key, boolean def) {
696 String s = get(key);
697 if (s != null && s.length() != 0)
698 return s.indexOf("\u001e") >= 0;
699 else
700 return def;
701 }
702
703 /**
704 * Get a list of values for a certain key
705 * @param key the identifier for the setting
706 * @param def the default value.
707 * @return the corresponding value if the property has been set before,
708 * def otherwise
709 */
710 synchronized public Collection<String> getCollection(String key, Collection<String> def) {
711 putCollectionDefault(key, def);
712 String s = get(key);
713 if(s != null && s.length() != 0)
714 return Arrays.asList(s.split("\u001e", -1));
715 return def;
716 }
717
718 /**
719 * Get a list of values for a certain key
720 * @param key the identifier for the setting
721 * @return the corresponding value if the property has been set before,
722 * an empty Collection otherwise.
723 */
724 synchronized public Collection<String> getCollection(String key) {
725 putCollectionDefault(key, null);
726 String s = get(key);
727 if (s != null && s.length() != 0)
728 return Arrays.asList(s.split("\u001e", -1));
729 return Collections.emptyList();
730 }
731
732 /* old style conversion, replace by above call after some transition time */
733 /* remove this function, when no more old-style preference collections in the code */
734 @Deprecated
735 synchronized public Collection<String> getCollectionOld(String key, String sep) {
736 putCollectionDefault(key, null);
737 String s = get(key);
738 if (s != null && s.length() != 0) {
739 if(!s.contains("\u001e") && s.contains(sep)) {
740 s = s.replace(sep, "\u001e");
741 put(key, s);
742 }
743 return Arrays.asList(s.split("\u001e", -1));
744 }
745 return Collections.emptyList();
746 }
747
748 synchronized public void removeFromCollection(String key, String value) {
749 List<String> a = new ArrayList<String>(getCollection(key, Collections.<String>emptyList()));
750 a.remove(value);
751 putCollection(key, a);
752 }
753
754 synchronized public boolean putCollection(String key, Collection<String> val) {
755 return put(key, Utils.join("\u001e", val));
756 }
757
758 /**
759 * Saves at most {@code maxsize} items of collection {@code val}.
760 */
761 public boolean putCollectionBounded(String key, int maxsize, Collection<String> val) {
762 Collection<String> newCollection = new ArrayList<String>(Math.min(maxsize, val.size()));
763 for (String i : val) {
764 if (newCollection.size() >= maxsize) {
765 break;
766 }
767 newCollection.add(i);
768 }
769 return putCollection(key, newCollection);
770 }
771
772 synchronized private void putCollectionDefault(String key, Collection<String> val) {
773 putDefault(key, Utils.join("\u001e", val));
774 }
775
776 /**
777 * Used to read a 2-dimensional array of strings from the preference file.
778 * If not a single entry could be found, def is returned.
779 */
780 synchronized public Collection<Collection<String>> getArray(String key,
781 Collection<Collection<String>> def)
782 {
783 if(def != null) {
784 putArrayDefault(key, def);
785 }
786 key += ".";
787 int num = 0;
788 Collection<Collection<String>> col = new LinkedList<Collection<String>>();
789 while(properties.containsKey(key+num)) {
790 col.add(getCollection(key+num++, null));
791 }
792 return num == 0 ? def : col;
793 }
794
795 synchronized public boolean putArray(String key, Collection<Collection<String>> val) {
796 boolean changed = false;
797 key += ".";
798 Collection<String> keys = getAllPrefix(key).keySet();
799 if(val != null) {
800 int num = 0;
801 for(Collection<String> c : val) {
802 keys.remove(key+num);
803 changed |= putCollection(key+num++, c);
804 }
805 }
806 int l = key.length();
807 for(String k : keys) {
808 try {
809 Integer.valueOf(k.substring(l));
810 changed |= put(k, null);
811 } catch(NumberFormatException e) {
812 /* everything which does not end with a number should not be deleted */
813 }
814 }
815 return changed;
816 }
817
818 synchronized private void putArrayDefault(String key, Collection<Collection<String>> val) {
819 key += ".";
820 Collection<String> keys = getAllPrefixDefault(key).keySet();
821 int num = 0;
822 for(Collection<String> c : val) {
823 keys.remove(key+num);
824 putCollectionDefault(key+num++, c);
825 }
826 int l = key.length();
827 for(String k : keys) {
828 try {
829 Integer.valueOf(k.substring(l));
830 defaults.remove(k);
831 } catch(Exception e) {
832 /* everything which does not end with a number should not be deleted */
833 }
834 }
835 }
836
837 @Retention(RetentionPolicy.RUNTIME) public @interface pref { }
838 @Retention(RetentionPolicy.RUNTIME) public @interface writeExplicitly { }
839
840 /**
841 * Get a list of hashes which are represented by a struct-like class.
842 * It reads lines of the form
843 * > key.0=prop:val \u001e prop:val \u001e ... \u001e prop:val
844 * > ...
845 * > key.N=prop:val \u001e prop:val \u001e ... \u001e prop:val
846 * Possible properties are given by fields of the class klass that have
847 * the @pref annotation.
848 * Default constructor is used to initialize the struct objects, properties
849 * then override some of these default values.
850 * @param key main preference key
851 * @param klass The struct class
852 * @return a list of objects of type T or an empty list if nothing was found
853 */
854 public <T> List<T> getListOfStructs(String key, Class<T> klass) {
855 List<T> r = getListOfStructs(key, null, klass);
856 if (r == null)
857 return Collections.emptyList();
858 else
859 return r;
860 }
861
862 /**
863 * same as above, but returns def if nothing was found
864 */
865 public <T> List<T> getListOfStructs(String key, Collection<T> def, Class<T> klass) {
866 Collection<Collection<String>> array =
867 getArray(key, def == null ? null : serializeListOfStructs(def, klass));
868 if (array == null)
869 return def == null ? null : new ArrayList<T>(def);
870 List<T> lst = new ArrayList<T>();
871 for (Collection<String> entries : array) {
872 T struct = deserializeStruct(entries, klass);
873 lst.add(struct);
874 }
875 return lst;
876 }
877
878 /**
879 * Save a list of hashes represented by a struct-like class.
880 * Considers only fields that have the @pref annotation.
881 * In addition it does not write fields with null values. (Thus they are cleared)
882 * Default values are given by the field values after default constructor has
883 * been called.
884 * Fields equal to the default value are not written unless the field has
885 * the @writeExplicitly annotation.
886 * @param key main preference key
887 * @param val the list that is supposed to be saved
888 * @param klass The struct class
889 * @return true if something has changed
890 */
891 public <T> boolean putListOfStructs(String key, Collection<T> val, Class<T> klass) {
892 return putArray(key, serializeListOfStructs(val, klass));
893 }
894
895 private <T> Collection<Collection<String>> serializeListOfStructs(Collection<T> l, Class<T> klass) {
896 if (l == null)
897 return null;
898 Collection<Collection<String>> vals = new ArrayList<Collection<String>>();
899 for (T struct : l) {
900 if (struct == null) {
901 continue;
902 }
903 vals.add(serializeStruct(struct, klass));
904 }
905 return vals;
906 }
907
908 private <T> Collection<String> serializeStruct(T struct, Class<T> klass) {
909 T structPrototype;
910 try {
911 structPrototype = klass.newInstance();
912 } catch (InstantiationException ex) {
913 throw new RuntimeException(ex);
914 } catch (IllegalAccessException ex) {
915 throw new RuntimeException(ex);
916 }
917
918 Collection<String> hash = new ArrayList<String>();
919 for (Field f : klass.getDeclaredFields()) {
920 if (f.getAnnotation(pref.class) == null) {
921 continue;
922 }
923 f.setAccessible(true);
924 try {
925 Object fieldValue = f.get(struct);
926 Object defaultFieldValue = f.get(structPrototype);
927 if (fieldValue != null) {
928 if (f.getAnnotation(writeExplicitly.class) != null || !Utils.equal(fieldValue, defaultFieldValue)) {
929 hash.add(String.format("%s:%s", f.getName().replace("_", "-"), fieldValue.toString()));
930 }
931 }
932 } catch (IllegalArgumentException ex) {
933 throw new RuntimeException();
934 } catch (IllegalAccessException ex) {
935 throw new RuntimeException();
936 }
937 }
938 return hash;
939 }
940
941 private <T> T deserializeStruct(Collection<String> hash, Class<T> klass) {
942 T struct = null;
943 try {
944 struct = klass.newInstance();
945 } catch (InstantiationException ex) {
946 throw new RuntimeException();
947 } catch (IllegalAccessException ex) {
948 throw new RuntimeException();
949 }
950 for (String key_value : hash) {
951 final int i = key_value.indexOf(':');
952 if (i == -1 || i == 0) {
953 continue;
954 }
955 String key = key_value.substring(0,i);
956 String valueString = key_value.substring(i+1);
957
958 Object value = null;
959 Field f;
960 try {
961 f = klass.getDeclaredField(key.replace("-", "_"));
962 } catch (NoSuchFieldException ex) {
963 continue;
964 } catch (SecurityException ex) {
965 throw new RuntimeException();
966 }
967 if (f.getAnnotation(pref.class) == null) {
968 continue;
969 }
970 f.setAccessible(true);
971 if (f.getType() == Boolean.class || f.getType() == boolean.class) {
972 value = Boolean.parseBoolean(valueString);
973 } else if (f.getType() == Integer.class || f.getType() == int.class) {
974 try {
975 value = Integer.parseInt(valueString);
976 } catch (NumberFormatException nfe) {
977 continue;
978 }
979 } else if (f.getType() == Double.class || f.getType() == double.class) {
980 try {
981 value = Double.parseDouble(valueString);
982 } catch (NumberFormatException nfe) {
983 continue;
984 }
985 } else if (f.getType() == String.class) {
986 value = valueString;
987 } else
988 throw new RuntimeException("unsupported preference primitive type");
989
990 try {
991 f.set(struct, value);
992 } catch (IllegalArgumentException ex) {
993 throw new AssertionError();
994 } catch (IllegalAccessException ex) {
995 throw new RuntimeException();
996 }
997 }
998 return struct;
999 }
1000
1001 /**
1002 * Updates system properties with the current values in the preferences.
1003 *
1004 */
1005 public void updateSystemProperties() {
1006 Properties sysProp = System.getProperties();
1007 sysProp.put("http.agent", Version.getInstance().getAgentString());
1008 System.setProperties(sysProp);
1009 }
1010
1011 /**
1012 * The default plugin site
1013 */
1014 private final static String[] DEFAULT_PLUGIN_SITE = {
1015 "http://josm.openstreetmap.de/plugin%<?plugins=>"};
1016
1017 /**
1018 * Replies the collection of plugin site URLs from where plugin lists can be downloaded
1019 *
1020 * @return
1021 */
1022 public Collection<String> getPluginSites() {
1023 return getCollection("pluginmanager.sites", Arrays.asList(DEFAULT_PLUGIN_SITE));
1024 }
1025
1026 /**
1027 * Sets the collection of plugin site URLs.
1028 *
1029 * @param sites the site URLs
1030 */
1031 public void setPluginSites(Collection<String> sites) {
1032 putCollection("pluginmanager.sites", sites);
1033 }
1034
1035 public static class XMLTag {
1036 public String key;
1037 public String value;
1038 }
1039 public static class XMLCollection {
1040 public String key;
1041 }
1042 public static class XMLEntry {
1043 public String value;
1044 }
1045 public void fromXML(Reader in) throws SAXException {
1046 XmlObjectParser parser = new XmlObjectParser();
1047 parser.map("tag", XMLTag.class);
1048 parser.map("entry", XMLEntry.class);
1049 parser.map("collection", XMLCollection.class);
1050 parser.startWithValidation(in,
1051 "http://josm.openstreetmap.de/preferences-1.0", "resource://data/preferences.xsd");
1052 LinkedList<String> vals = new LinkedList<String>();
1053 while(parser.hasNext()) {
1054 Object o = parser.next();
1055 if(o instanceof XMLTag) {
1056 properties.put(((XMLTag)o).key, ((XMLTag)o).value);
1057 } else if (o instanceof XMLEntry) {
1058 vals.add(((XMLEntry)o).value);
1059 } else if (o instanceof XMLCollection) {
1060 properties.put(((XMLCollection)o).key, Utils.join("\u001e", vals));
1061 vals = new LinkedList<String>();
1062 }
1063 }
1064 }
1065
1066 public String toXML(boolean nopass) {
1067 StringBuilder b = new StringBuilder(
1068 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
1069 "<preferences xmlns=\"http://josm.openstreetmap.de/preferences-1.0\">\n");
1070 for (Entry<String, String> p : properties.entrySet()) {
1071 if (nopass && p.getKey().equals("osm-server.password")) {
1072 continue; // do not store plain password.
1073 }
1074 String r = p.getValue();
1075 if(r.contains("\u001e"))
1076 {
1077 b.append(" <collection key='");
1078 b.append(XmlWriter.encode(p.getKey()));
1079 b.append("'>\n");
1080 for (String val : r.split("\u001e", -1))
1081 {
1082 b.append(" <entry value='");
1083 b.append(XmlWriter.encode(val));
1084 b.append("' />\n");
1085 }
1086 b.append(" </collection>\n");
1087 }
1088 else
1089 {
1090 b.append(" <tag key='");
1091 b.append(XmlWriter.encode(p.getKey()));
1092 b.append("' value='");
1093 b.append(XmlWriter.encode(p.getValue()));
1094 b.append("' />\n");
1095 }
1096 }
1097 b.append("</preferences>");
1098 return b.toString();
1099 }
1100}
Note: See TracBrowser for help on using the repository browser.