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

Last change on this file since 4554 was 4554, checked in by stoecker, 12 years ago

temporary disable XML saving till next tested is released - fix #7027

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