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

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

remove old debug stuff

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