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

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

allow to color the entries in layer list dialog according to assigned layer drawing color

  • Property svn:eol-style set to native
File size: 37.8 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;
36
37import org.openstreetmap.josm.Main;
38import org.openstreetmap.josm.io.XmlWriter;
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 synchronized public boolean getBoolean(final String key, final String specName, final boolean def) {
293 putDefault(key, Boolean.toString(def));
294 String skey = key+"."+specName;
295 if(properties.containsKey(skey))
296 return Boolean.parseBoolean(properties.get(skey));
297 return properties.containsKey(key) ? Boolean.parseBoolean(properties.get(key)) : def;
298 }
299
300 /**
301 * Set a value for a certain setting. The changed setting is saved
302 * to the preference file immediately. Due to caching mechanisms on modern
303 * operating systems and hardware, this shouldn't be a performance problem.
304 * @param key the unique identifier for the setting
305 * @param value the value of the setting. Can be null or "" wich both removes
306 * the key-value entry.
307 * @return if true, something has changed (i.e. value is different than before)
308 */
309 public boolean put(final String key, String value) {
310 boolean changed = false;
311 String oldValue = null;
312
313 synchronized (this) {
314 oldValue = properties.get(key);
315 if(value != null && value.length() == 0) {
316 value = null;
317 }
318 // value is the same as before - no need to save anything
319 boolean equalValue = oldValue != null && oldValue.equals(value);
320 // The setting was previously unset and we are supposed to put a
321 // value that equals the default value. This is not necessary because
322 // the default value is the same throughout josm. In addition we like
323 // to have the possibility to change the default value from version
324 // to version, which would not work if we wrote it to the preference file.
325 boolean unsetIsDefault = oldValue == null && (value == null || value.equals(defaults.get(key)));
326
327 if (!(equalValue || unsetIsDefault)) {
328 if (value == null) {
329 properties.remove(key);
330 } else {
331 properties.put(key, value);
332 }
333 try {
334 save();
335 } catch(IOException e){
336 System.out.println(tr("Warning: failed to persist preferences to ''{0}''", getPreferenceFile().getAbsoluteFile()));
337 }
338 changed = true;
339 }
340 }
341 if (changed) {
342 // Call outside of synchronized section in case some listener wait for other thread that wait for preference lock
343 firePreferenceChanged(key, oldValue, value);
344 }
345 return changed;
346 }
347
348 public boolean put(final String key, final boolean value) {
349 return put(key, Boolean.toString(value));
350 }
351
352 public boolean putInteger(final String key, final Integer value) {
353 return put(key, Integer.toString(value));
354 }
355
356 public boolean putDouble(final String key, final Double value) {
357 return put(key, Double.toString(value));
358 }
359
360 public boolean putLong(final String key, final Long value) {
361 return put(key, Long.toString(value));
362 }
363
364 /**
365 * Called after every put. In case of a problem, do nothing but output the error
366 * in log.
367 */
368 public void save() throws IOException {
369 /* currently unused, but may help to fix configuration issues in future */
370 putInteger("josm.version", Version.getInstance().getVersion());
371
372 updateSystemProperties();
373 if(Main.applet)
374 return;
375
376 File prefFile = getPreferenceFile();
377 File backupFile = new File(prefFile + "_backup");
378
379 // Backup old preferences if there are old preferences
380 if(prefFile.exists()) {
381 copyFile(prefFile, backupFile);
382 }
383
384 final PrintWriter out = new PrintWriter(new OutputStreamWriter(
385 new FileOutputStream(prefFile + "_tmp"), "utf-8"), false);
386 for (final Entry<String, String> e : properties.entrySet()) {
387 String s = defaults.get(e.getKey());
388 /* don't save default values */
389 if(s == null || !s.equals(e.getValue())) {
390 out.println(e.getKey() + "=" + e.getValue());
391 }
392 }
393 out.close();
394
395 File tmpFile = new File(prefFile + "_tmp");
396 copyFile(tmpFile, prefFile);
397 tmpFile.delete();
398
399 setCorrectPermissions(prefFile);
400 setCorrectPermissions(backupFile);
401 }
402
403
404 private void setCorrectPermissions(File file) {
405 file.setReadable(false, false);
406 file.setWritable(false, false);
407 file.setExecutable(false, false);
408 file.setReadable(true, true);
409 file.setWritable(true, true);
410 }
411
412 /**
413 * Simple file copy function that will overwrite the target file
414 * Taken from http://www.rgagnon.com/javadetails/java-0064.html (CC-NC-BY-SA)
415 * @param in
416 * @param out
417 * @throws IOException
418 */
419 public static void copyFile(File in, File out) throws IOException {
420 FileChannel inChannel = new FileInputStream(in).getChannel();
421 FileChannel outChannel = new FileOutputStream(out).getChannel();
422 try {
423 inChannel.transferTo(0, inChannel.size(),
424 outChannel);
425 }
426 catch (IOException e) {
427 throw e;
428 }
429 finally {
430 if (inChannel != null) {
431 inChannel.close();
432 }
433 if (outChannel != null) {
434 outChannel.close();
435 }
436 }
437 }
438
439 public void load() throws IOException {
440 properties.clear();
441 if(!Main.applet) {
442 final BufferedReader in = new BufferedReader(new InputStreamReader(
443 new FileInputStream(getPreferencesDir()+"preferences"), "utf-8"));
444 int lineNumber = 0;
445 ArrayList<Integer> errLines = new ArrayList<Integer>();
446 for (String line = in.readLine(); line != null; line = in.readLine(), lineNumber++) {
447 final int i = line.indexOf('=');
448 if (i == -1 || i == 0) {
449 errLines.add(lineNumber);
450 continue;
451 }
452 String key = line.substring(0,i);
453 String value = line.substring(i+1);
454 if (!value.isEmpty()) {
455 properties.put(key, value);
456 }
457 }
458 if (!errLines.isEmpty())
459 throw new IOException(tr("Malformed config file at lines {0}", errLines));
460 }
461 updateSystemProperties();
462 }
463
464 public void init(boolean reset){
465 if(Main.applet)
466 return;
467 // get the preferences.
468 File prefDir = getPreferencesDirFile();
469 if (prefDir.exists()) {
470 if(!prefDir.isDirectory()) {
471 System.err.println(tr("Warning: Failed to initialize preferences. Preference directory ''{0}'' is not a directory.", prefDir.getAbsoluteFile()));
472 JOptionPane.showMessageDialog(
473 Main.parent,
474 tr("<html>Failed to initialize preferences.<br>Preference directory ''{0}'' is not a directory.</html>", prefDir.getAbsoluteFile()),
475 tr("Error"),
476 JOptionPane.ERROR_MESSAGE
477 );
478 return;
479 }
480 } else {
481 if (! prefDir.mkdirs()) {
482 System.err.println(tr("Warning: Failed to initialize preferences. Failed to create missing preference directory: {0}", prefDir.getAbsoluteFile()));
483 JOptionPane.showMessageDialog(
484 Main.parent,
485 tr("<html>Failed to initialize preferences.<br>Failed to create missing preference directory: {0}</html>",prefDir.getAbsoluteFile()),
486 tr("Error"),
487 JOptionPane.ERROR_MESSAGE
488 );
489 return;
490 }
491 }
492
493 File preferenceFile = getPreferenceFile();
494 try {
495 if (!preferenceFile.exists()) {
496 System.out.println(tr("Warning: Missing preference file ''{0}''. Creating a default preference file.", preferenceFile.getAbsoluteFile()));
497 resetToDefault();
498 save();
499 } else if (reset) {
500 System.out.println(tr("Warning: Replacing existing preference file ''{0}'' with default preference file.", preferenceFile.getAbsoluteFile()));
501 resetToDefault();
502 save();
503 }
504 } catch(IOException e) {
505 e.printStackTrace();
506 JOptionPane.showMessageDialog(
507 Main.parent,
508 tr("<html>Failed to initialize preferences.<br>Failed to reset preference file to default: {0}</html>",getPreferenceFile().getAbsoluteFile()),
509 tr("Error"),
510 JOptionPane.ERROR_MESSAGE
511 );
512 return;
513 }
514 try {
515 load();
516 } catch (IOException e) {
517 e.printStackTrace();
518 File backupFile = new File(prefDir,"preferences.bak");
519 JOptionPane.showMessageDialog(
520 Main.parent,
521 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()),
522 tr("Error"),
523 JOptionPane.ERROR_MESSAGE
524 );
525 preferenceFile.renameTo(backupFile);
526 try {
527 resetToDefault();
528 save();
529 } catch(IOException e1) {
530 e1.printStackTrace();
531 System.err.println(tr("Warning: Failed to initialize preferences.Failed to reset preference file to default: {0}", getPreferenceFile()));
532 }
533 }
534 }
535
536 public final void resetToDefault(){
537 properties.clear();
538 }
539
540 /**
541 * Convenience method for accessing colour preferences.
542 *
543 * @param colName name of the colour
544 * @param def default value
545 * @return a Color object for the configured colour, or the default value if none configured.
546 */
547 synchronized public Color getColor(String colName, Color def) {
548 return getColor(colName, null, def);
549 }
550
551 /* only for preferences */
552 synchronized public String getColorName(String o) {
553 try
554 {
555 Matcher m = Pattern.compile("mappaint\\.(.+?)\\.(.+)").matcher(o);
556 m.matches();
557 return tr("Paint style {0}: {1}", tr(m.group(1)), tr(m.group(2)));
558 }
559 catch (Exception e) {}
560 try
561 {
562 Matcher m = Pattern.compile("layer (.+)").matcher(o);
563 m.matches();
564 return tr("Layer: {0}", tr(m.group(1)));
565 }
566 catch (Exception e) {}
567 return tr(colornames.containsKey(o) ? colornames.get(o) : o);
568 }
569
570 public Color getColor(ColorKey key) {
571 return getColor(key.getColorName(), key.getSpecialName(), key.getDefault());
572 }
573
574 /**
575 * Convenience method for accessing colour preferences.
576 *
577 * @param colName name of the colour
578 * @param specName name of the special colour settings
579 * @param def default value
580 * @return a Color object for the configured colour, or the default value if none configured.
581 */
582 synchronized public Color getColor(String colName, String specName, Color def) {
583 String colKey = colName.toLowerCase().replaceAll("[^a-z0-9]+",".");
584 if(!colKey.equals(colName)) {
585 colornames.put(colKey, colName);
586 }
587 putDefault("color."+colKey, ColorHelper.color2html(def));
588 String colStr = specName != null ? get("color."+specName) : "";
589 if(colStr.equals("")) {
590 colStr = get("color."+colKey);
591 }
592 return colStr.equals("") ? def : ColorHelper.html2color(colStr);
593 }
594
595 synchronized public Color getDefaultColor(String colName) {
596 String colStr = defaults.get("color."+colName);
597 return colStr == null || "".equals(colStr) ? null : ColorHelper.html2color(colStr);
598 }
599
600 synchronized public boolean putColor(String colName, Color val) {
601 return put("color."+colName, val != null ? ColorHelper.color2html(val) : null);
602 }
603
604 synchronized public int getInteger(String key, int def) {
605 putDefault(key, Integer.toString(def));
606 String v = get(key);
607 if(null == v)
608 return def;
609
610 try {
611 return Integer.parseInt(v);
612 } catch(NumberFormatException e) {
613 // fall out
614 }
615 return def;
616 }
617
618 synchronized public int getInteger(String key, String specName, int def) {
619 putDefault(key, Integer.toString(def));
620 String v = get(key+"."+specName);
621 if(null == v)
622 v = get(key);
623 if(null == v)
624 return def;
625
626 try {
627 return Integer.parseInt(v);
628 } catch(NumberFormatException e) {
629 // fall out
630 }
631 return def;
632 }
633
634 synchronized public long getLong(String key, long def) {
635 putDefault(key, Long.toString(def));
636 String v = get(key);
637 if(null == v)
638 return def;
639
640 try {
641 return Long.parseLong(v);
642 } catch(NumberFormatException e) {
643 // fall out
644 }
645 return def;
646 }
647
648 synchronized public double getDouble(String key, double def) {
649 putDefault(key, Double.toString(def));
650 String v = get(key);
651 if(null == v)
652 return def;
653
654 try {
655 return Double.parseDouble(v);
656 } catch(NumberFormatException e) {
657 // fall out
658 }
659 return def;
660 }
661
662 synchronized public double getDouble(String key, String def) {
663 putDefault(key, def);
664 String v = get(key);
665 if(v != null && v.length() != 0) {
666 try { return Double.parseDouble(v); } catch(NumberFormatException e) {}
667 }
668 try { return Double.parseDouble(def); } catch(NumberFormatException e) {}
669 return 0.0;
670 }
671
672 synchronized public String getCollectionAsString(final String key) {
673 String s = get(key);
674 if(s != null && s.length() != 0) {
675 s = s.replaceAll("\u001e",",");
676 }
677 return s;
678 }
679
680 public boolean isCollection(String key, boolean def) {
681 String s = get(key);
682 if (s != null && s.length() != 0)
683 return s.indexOf("\u001e") >= 0;
684 else
685 return def;
686 }
687
688 /**
689 * Get a list of values for a certain key
690 * @param key the identifier for the setting
691 * @param def the default value.
692 * @return the corresponding value if the property has been set before,
693 * def otherwise
694 */
695 synchronized public Collection<String> getCollection(String key, Collection<String> def) {
696 putCollectionDefault(key, def);
697 String s = get(key);
698 if(s != null && s.length() != 0)
699 return Arrays.asList(s.split("\u001e", -1));
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 * @return the corresponding value if the property has been set before,
707 * an empty Collection otherwise.
708 */
709 synchronized public Collection<String> getCollection(String key) {
710 putCollectionDefault(key, null);
711 String s = get(key);
712 if (s != null && s.length() != 0)
713 return Arrays.asList(s.split("\u001e", -1));
714 return Collections.emptyList();
715 }
716
717 /* old style conversion, replace by above call after some transition time */
718 /* remove this function, when no more old-style preference collections in the code */
719 @Deprecated
720 synchronized public Collection<String> getCollectionOld(String key, String sep) {
721 putCollectionDefault(key, null);
722 String s = get(key);
723 if (s != null && s.length() != 0) {
724 if(!s.contains("\u001e") && s.contains(sep)) {
725 s = s.replace(sep, "\u001e");
726 put(key, s);
727 }
728 return Arrays.asList(s.split("\u001e", -1));
729 }
730 return Collections.emptyList();
731 }
732
733 synchronized public void removeFromCollection(String key, String value) {
734 List<String> a = new ArrayList<String>(getCollection(key, Collections.<String>emptyList()));
735 a.remove(value);
736 putCollection(key, a);
737 }
738
739 synchronized public boolean putCollection(String key, Collection<String> val) {
740 return put(key, Utils.join("\u001e", val));
741 }
742
743 synchronized private void putCollectionDefault(String key, Collection<String> val) {
744 putDefault(key, Utils.join("\u001e", val));
745 }
746
747 /**
748 * Used to read a 2-dimensional array of strings from the preference file.
749 * If not a single entry could be found, def is returned.
750 */
751 synchronized public Collection<Collection<String>> getArray(String key,
752 Collection<Collection<String>> def)
753 {
754 if(def != null) {
755 putArrayDefault(key, def);
756 }
757 key += ".";
758 int num = 0;
759 Collection<Collection<String>> col = new LinkedList<Collection<String>>();
760 while(properties.containsKey(key+num)) {
761 col.add(getCollection(key+num++, null));
762 }
763 return num == 0 ? def : col;
764 }
765
766 synchronized public boolean putArray(String key, Collection<Collection<String>> val) {
767 boolean changed = false;
768 key += ".";
769 Collection<String> keys = getAllPrefix(key).keySet();
770 if(val != null) {
771 int num = 0;
772 for(Collection<String> c : val) {
773 keys.remove(key+num);
774 changed |= putCollection(key+num++, c);
775 }
776 }
777 int l = key.length();
778 for(String k : keys) {
779 try {
780 Integer.valueOf(k.substring(l));
781 changed |= put(k, null);
782 } catch(NumberFormatException e) {
783 /* everything which does not end with a number should not be deleted */
784 }
785 }
786 return changed;
787 }
788
789 synchronized private void putArrayDefault(String key, Collection<Collection<String>> val) {
790 key += ".";
791 Collection<String> keys = getAllPrefixDefault(key).keySet();
792 int num = 0;
793 for(Collection<String> c : val) {
794 keys.remove(key+num);
795 putCollectionDefault(key+num++, c);
796 }
797 int l = key.length();
798 for(String k : keys) {
799 try {
800 Integer.valueOf(k.substring(l));
801 defaults.remove(k);
802 } catch(Exception e) {
803 /* everything which does not end with a number should not be deleted */
804 }
805 }
806 }
807
808 @Retention(RetentionPolicy.RUNTIME) public @interface pref { }
809 @Retention(RetentionPolicy.RUNTIME) public @interface writeExplicitly { }
810
811 /**
812 * Get a list of hashes which are represented by a struct-like class.
813 * It reads lines of the form
814 * > key.0=prop:val \u001e prop:val \u001e ... \u001e prop:val
815 * > ...
816 * > key.N=prop:val \u001e prop:val \u001e ... \u001e prop:val
817 * Possible properties are given by fields of the class klass that have
818 * the @pref annotation.
819 * Default constructor is used to initialize the struct objects, properties
820 * then override some of these default values.
821 * @param key main preference key
822 * @param klass The struct class
823 * @return a list of objects of type T or an empty list if nothing was found
824 */
825 public <T> List<T> getListOfStructs(String key, Class<T> klass) {
826 List<T> r = getListOfStructs(key, null, klass);
827 if (r == null)
828 return Collections.emptyList();
829 else
830 return r;
831 }
832
833 /**
834 * same as above, but returns def if nothing was found
835 */
836 public <T> List<T> getListOfStructs(String key, Collection<T> def, Class<T> klass) {
837 Collection<Collection<String>> array =
838 getArray(key, def == null ? null : serializeListOfStructs(def, klass));
839 if (array == null)
840 return def == null ? null : new ArrayList<T>(def);
841 List<T> lst = new ArrayList<T>();
842 for (Collection<String> entries : array) {
843 T struct = deserializeStruct(entries, klass);
844 lst.add(struct);
845 }
846 return lst;
847 }
848
849 /**
850 * Save a list of hashes represented by a struct-like class.
851 * Considers only fields that have the @pref annotation.
852 * In addition it does not write fields with null values. (Thus they are cleared)
853 * Default values are given by the field values after default constructor has
854 * been called.
855 * Fields equal to the default value are not written unless the field has
856 * the @writeExplicitly annotation.
857 * @param key main preference key
858 * @param val the list that is supposed to be saved
859 * @param klass The struct class
860 * @return true if something has changed
861 */
862 public <T> boolean putListOfStructs(String key, Collection<T> val, Class<T> klass) {
863 return putArray(key, serializeListOfStructs(val, klass));
864 }
865
866 private <T> Collection<Collection<String>> serializeListOfStructs(Collection<T> l, Class<T> klass) {
867 if (l == null)
868 return null;
869 Collection<Collection<String>> vals = new ArrayList<Collection<String>>();
870 for (T struct : l) {
871 if (struct == null) {
872 continue;
873 }
874 vals.add(serializeStruct(struct, klass));
875 }
876 return vals;
877 }
878
879 private <T> Collection<String> serializeStruct(T struct, Class<T> klass) {
880 T structPrototype;
881 try {
882 structPrototype = klass.newInstance();
883 } catch (InstantiationException ex) {
884 throw new RuntimeException();
885 } catch (IllegalAccessException ex) {
886 throw new RuntimeException();
887 }
888
889 Collection<String> hash = new ArrayList<String>();
890 for (Field f : klass.getDeclaredFields()) {
891 if (f.getAnnotation(pref.class) == null) {
892 continue;
893 }
894 f.setAccessible(true);
895 try {
896 Object fieldValue = f.get(struct);
897 Object defaultFieldValue = f.get(structPrototype);
898 if (fieldValue != null) {
899 if (f.getAnnotation(writeExplicitly.class) != null || !Utils.equal(fieldValue, defaultFieldValue)) {
900 hash.add(String.format("%s:%s", f.getName().replace("_", "-"), fieldValue.toString()));
901 }
902 }
903 } catch (IllegalArgumentException ex) {
904 throw new RuntimeException();
905 } catch (IllegalAccessException ex) {
906 throw new RuntimeException();
907 }
908 }
909 return hash;
910 }
911
912 private <T> T deserializeStruct(Collection<String> hash, Class<T> klass) {
913 T struct = null;
914 try {
915 struct = klass.newInstance();
916 } catch (InstantiationException ex) {
917 throw new RuntimeException();
918 } catch (IllegalAccessException ex) {
919 throw new RuntimeException();
920 }
921 for (String key_value : hash) {
922 final int i = key_value.indexOf(':');
923 if (i == -1 || i == 0) {
924 continue;
925 }
926 String key = key_value.substring(0,i);
927 String valueString = key_value.substring(i+1);
928
929 Object value = null;
930 Field f;
931 try {
932 f = klass.getDeclaredField(key.replace("-", "_"));
933 } catch (NoSuchFieldException ex) {
934 continue;
935 } catch (SecurityException ex) {
936 throw new RuntimeException();
937 }
938 if (f.getAnnotation(pref.class) == null) {
939 continue;
940 }
941 f.setAccessible(true);
942 if (f.getType() == Boolean.class || f.getType() == boolean.class) {
943 value = Boolean.parseBoolean(valueString);
944 } else if (f.getType() == Integer.class || f.getType() == int.class) {
945 try {
946 value = Integer.parseInt(valueString);
947 } catch (NumberFormatException nfe) {
948 continue;
949 }
950 } else if (f.getType() == String.class) {
951 value = valueString;
952 } else
953 throw new RuntimeException("unsupported preference primitive type");
954
955 try {
956 f.set(struct, value);
957 } catch (IllegalArgumentException ex) {
958 throw new AssertionError();
959 } catch (IllegalAccessException ex) {
960 throw new RuntimeException();
961 }
962 }
963 return struct;
964 }
965
966 /**
967 * Updates system properties with the current values in the preferences.
968 *
969 */
970 public void updateSystemProperties() {
971 Properties sysProp = System.getProperties();
972 sysProp.put("http.agent", Version.getInstance().getAgentString());
973 System.setProperties(sysProp);
974 }
975
976 /**
977 * The default plugin site
978 */
979 private final static String[] DEFAULT_PLUGIN_SITE = {
980 "http://josm.openstreetmap.de/plugin%<?plugins=>"};
981
982 /**
983 * Replies the collection of plugin site URLs from where plugin lists can be downloaded
984 *
985 * @return
986 */
987 public Collection<String> getPluginSites() {
988 return getCollection("pluginmanager.sites", Arrays.asList(DEFAULT_PLUGIN_SITE));
989 }
990
991 /**
992 * Sets the collection of plugin site URLs.
993 *
994 * @param sites the site URLs
995 */
996 public void setPluginSites(Collection<String> sites) {
997 putCollection("pluginmanager.sites", sites);
998 }
999
1000 public static class XMLTag {
1001 public String key;
1002 public String value;
1003 }
1004 public static class XMLCollection {
1005 public String key;
1006 }
1007 public static class XMLEntry {
1008 public String value;
1009 }
1010 public void fromXML(Reader in) throws SAXException {
1011 XmlObjectParser parser = new XmlObjectParser();
1012 parser.map("tag", XMLTag.class);
1013 parser.map("entry", XMLEntry.class);
1014 parser.map("collection", XMLCollection.class);
1015 parser.startWithValidation(in,
1016 "http://josm.openstreetmap.de/preferences-1.0", "resource://data/preferences.xsd");
1017 LinkedList<String> vals = new LinkedList<String>();
1018 while(parser.hasNext()) {
1019 Object o = parser.next();
1020 if(o instanceof XMLTag) {
1021 properties.put(((XMLTag)o).key, ((XMLTag)o).value);
1022 } else if (o instanceof XMLEntry) {
1023 vals.add(((XMLEntry)o).value);
1024 } else if (o instanceof XMLCollection) {
1025 properties.put(((XMLCollection)o).key, Utils.join("\u001e", vals));
1026 vals = new LinkedList<String>();
1027 }
1028 }
1029 }
1030
1031 public String toXML(boolean nopass) {
1032 StringBuilder b = new StringBuilder(
1033 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
1034 "<preferences xmlns=\"http://josm.openstreetmap.de/preferences-1.0\">\n");
1035 for (Entry<String, String> p : properties.entrySet()) {
1036 if (nopass && p.getKey().equals("osm-server.password")) {
1037 continue; // do not store plain password.
1038 }
1039 String r = p.getValue();
1040 if(r.contains("\u001e"))
1041 {
1042 b.append(" <collection key='");
1043 b.append(XmlWriter.encode(p.getKey()));
1044 b.append("'>\n");
1045 for (String val : r.split("\u001e", -1))
1046 {
1047 b.append(" <entry value='");
1048 b.append(XmlWriter.encode(val));
1049 b.append("' />\n");
1050 }
1051 b.append(" </collection>\n");
1052 }
1053 else
1054 {
1055 b.append(" <tag key='");
1056 b.append(XmlWriter.encode(p.getKey()));
1057 b.append("' value='");
1058 b.append(XmlWriter.encode(p.getValue()));
1059 b.append("' />\n");
1060 }
1061 }
1062 b.append("</preferences>");
1063 return b.toString();
1064 }
1065}
Note: See TracBrowser for help on using the repository browser.