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

Last change on this file since 4371 was 4371, checked in by simon04, 13 years ago

fix #6728 - add saved files to "open recent" menu

  • Property svn:eol-style set to native
File size: 38.3 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(v.isEmpty())
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(v.isEmpty())
622 v = get(key);
623 if(v.isEmpty())
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 /**
744 * Saves at most {@code maxsize} items of collection {@code val}.
745 */
746 public boolean putCollectionBounded(String key, int maxsize, Collection<String> val) {
747 Collection<String> newCollection = new ArrayList<String>(maxsize);
748 for (String i : val) {
749 if (newCollection.size() >= maxsize) {
750 break;
751 }
752 newCollection.add(i);
753 }
754 return putCollection(key, newCollection);
755 }
756
757 synchronized private void putCollectionDefault(String key, Collection<String> val) {
758 putDefault(key, Utils.join("\u001e", val));
759 }
760
761 /**
762 * Used to read a 2-dimensional array of strings from the preference file.
763 * If not a single entry could be found, def is returned.
764 */
765 synchronized public Collection<Collection<String>> getArray(String key,
766 Collection<Collection<String>> def)
767 {
768 if(def != null) {
769 putArrayDefault(key, def);
770 }
771 key += ".";
772 int num = 0;
773 Collection<Collection<String>> col = new LinkedList<Collection<String>>();
774 while(properties.containsKey(key+num)) {
775 col.add(getCollection(key+num++, null));
776 }
777 return num == 0 ? def : col;
778 }
779
780 synchronized public boolean putArray(String key, Collection<Collection<String>> val) {
781 boolean changed = false;
782 key += ".";
783 Collection<String> keys = getAllPrefix(key).keySet();
784 if(val != null) {
785 int num = 0;
786 for(Collection<String> c : val) {
787 keys.remove(key+num);
788 changed |= putCollection(key+num++, c);
789 }
790 }
791 int l = key.length();
792 for(String k : keys) {
793 try {
794 Integer.valueOf(k.substring(l));
795 changed |= put(k, null);
796 } catch(NumberFormatException e) {
797 /* everything which does not end with a number should not be deleted */
798 }
799 }
800 return changed;
801 }
802
803 synchronized private void putArrayDefault(String key, Collection<Collection<String>> val) {
804 key += ".";
805 Collection<String> keys = getAllPrefixDefault(key).keySet();
806 int num = 0;
807 for(Collection<String> c : val) {
808 keys.remove(key+num);
809 putCollectionDefault(key+num++, c);
810 }
811 int l = key.length();
812 for(String k : keys) {
813 try {
814 Integer.valueOf(k.substring(l));
815 defaults.remove(k);
816 } catch(Exception e) {
817 /* everything which does not end with a number should not be deleted */
818 }
819 }
820 }
821
822 @Retention(RetentionPolicy.RUNTIME) public @interface pref { }
823 @Retention(RetentionPolicy.RUNTIME) public @interface writeExplicitly { }
824
825 /**
826 * Get a list of hashes which are represented by a struct-like class.
827 * It reads lines of the form
828 * > key.0=prop:val \u001e prop:val \u001e ... \u001e prop:val
829 * > ...
830 * > key.N=prop:val \u001e prop:val \u001e ... \u001e prop:val
831 * Possible properties are given by fields of the class klass that have
832 * the @pref annotation.
833 * Default constructor is used to initialize the struct objects, properties
834 * then override some of these default values.
835 * @param key main preference key
836 * @param klass The struct class
837 * @return a list of objects of type T or an empty list if nothing was found
838 */
839 public <T> List<T> getListOfStructs(String key, Class<T> klass) {
840 List<T> r = getListOfStructs(key, null, klass);
841 if (r == null)
842 return Collections.emptyList();
843 else
844 return r;
845 }
846
847 /**
848 * same as above, but returns def if nothing was found
849 */
850 public <T> List<T> getListOfStructs(String key, Collection<T> def, Class<T> klass) {
851 Collection<Collection<String>> array =
852 getArray(key, def == null ? null : serializeListOfStructs(def, klass));
853 if (array == null)
854 return def == null ? null : new ArrayList<T>(def);
855 List<T> lst = new ArrayList<T>();
856 for (Collection<String> entries : array) {
857 T struct = deserializeStruct(entries, klass);
858 lst.add(struct);
859 }
860 return lst;
861 }
862
863 /**
864 * Save a list of hashes represented by a struct-like class.
865 * Considers only fields that have the @pref annotation.
866 * In addition it does not write fields with null values. (Thus they are cleared)
867 * Default values are given by the field values after default constructor has
868 * been called.
869 * Fields equal to the default value are not written unless the field has
870 * the @writeExplicitly annotation.
871 * @param key main preference key
872 * @param val the list that is supposed to be saved
873 * @param klass The struct class
874 * @return true if something has changed
875 */
876 public <T> boolean putListOfStructs(String key, Collection<T> val, Class<T> klass) {
877 return putArray(key, serializeListOfStructs(val, klass));
878 }
879
880 private <T> Collection<Collection<String>> serializeListOfStructs(Collection<T> l, Class<T> klass) {
881 if (l == null)
882 return null;
883 Collection<Collection<String>> vals = new ArrayList<Collection<String>>();
884 for (T struct : l) {
885 if (struct == null) {
886 continue;
887 }
888 vals.add(serializeStruct(struct, klass));
889 }
890 return vals;
891 }
892
893 private <T> Collection<String> serializeStruct(T struct, Class<T> klass) {
894 T structPrototype;
895 try {
896 structPrototype = klass.newInstance();
897 } catch (InstantiationException ex) {
898 throw new RuntimeException();
899 } catch (IllegalAccessException ex) {
900 throw new RuntimeException();
901 }
902
903 Collection<String> hash = new ArrayList<String>();
904 for (Field f : klass.getDeclaredFields()) {
905 if (f.getAnnotation(pref.class) == null) {
906 continue;
907 }
908 f.setAccessible(true);
909 try {
910 Object fieldValue = f.get(struct);
911 Object defaultFieldValue = f.get(structPrototype);
912 if (fieldValue != null) {
913 if (f.getAnnotation(writeExplicitly.class) != null || !Utils.equal(fieldValue, defaultFieldValue)) {
914 hash.add(String.format("%s:%s", f.getName().replace("_", "-"), fieldValue.toString()));
915 }
916 }
917 } catch (IllegalArgumentException ex) {
918 throw new RuntimeException();
919 } catch (IllegalAccessException ex) {
920 throw new RuntimeException();
921 }
922 }
923 return hash;
924 }
925
926 private <T> T deserializeStruct(Collection<String> hash, Class<T> klass) {
927 T struct = null;
928 try {
929 struct = klass.newInstance();
930 } catch (InstantiationException ex) {
931 throw new RuntimeException();
932 } catch (IllegalAccessException ex) {
933 throw new RuntimeException();
934 }
935 for (String key_value : hash) {
936 final int i = key_value.indexOf(':');
937 if (i == -1 || i == 0) {
938 continue;
939 }
940 String key = key_value.substring(0,i);
941 String valueString = key_value.substring(i+1);
942
943 Object value = null;
944 Field f;
945 try {
946 f = klass.getDeclaredField(key.replace("-", "_"));
947 } catch (NoSuchFieldException ex) {
948 continue;
949 } catch (SecurityException ex) {
950 throw new RuntimeException();
951 }
952 if (f.getAnnotation(pref.class) == null) {
953 continue;
954 }
955 f.setAccessible(true);
956 if (f.getType() == Boolean.class || f.getType() == boolean.class) {
957 value = Boolean.parseBoolean(valueString);
958 } else if (f.getType() == Integer.class || f.getType() == int.class) {
959 try {
960 value = Integer.parseInt(valueString);
961 } catch (NumberFormatException nfe) {
962 continue;
963 }
964 } else if (f.getType() == String.class) {
965 value = valueString;
966 } else
967 throw new RuntimeException("unsupported preference primitive type");
968
969 try {
970 f.set(struct, value);
971 } catch (IllegalArgumentException ex) {
972 throw new AssertionError();
973 } catch (IllegalAccessException ex) {
974 throw new RuntimeException();
975 }
976 }
977 return struct;
978 }
979
980 /**
981 * Updates system properties with the current values in the preferences.
982 *
983 */
984 public void updateSystemProperties() {
985 Properties sysProp = System.getProperties();
986 sysProp.put("http.agent", Version.getInstance().getAgentString());
987 System.setProperties(sysProp);
988 }
989
990 /**
991 * The default plugin site
992 */
993 private final static String[] DEFAULT_PLUGIN_SITE = {
994 "http://josm.openstreetmap.de/plugin%<?plugins=>"};
995
996 /**
997 * Replies the collection of plugin site URLs from where plugin lists can be downloaded
998 *
999 * @return
1000 */
1001 public Collection<String> getPluginSites() {
1002 return getCollection("pluginmanager.sites", Arrays.asList(DEFAULT_PLUGIN_SITE));
1003 }
1004
1005 /**
1006 * Sets the collection of plugin site URLs.
1007 *
1008 * @param sites the site URLs
1009 */
1010 public void setPluginSites(Collection<String> sites) {
1011 putCollection("pluginmanager.sites", sites);
1012 }
1013
1014 public static class XMLTag {
1015 public String key;
1016 public String value;
1017 }
1018 public static class XMLCollection {
1019 public String key;
1020 }
1021 public static class XMLEntry {
1022 public String value;
1023 }
1024 public void fromXML(Reader in) throws SAXException {
1025 XmlObjectParser parser = new XmlObjectParser();
1026 parser.map("tag", XMLTag.class);
1027 parser.map("entry", XMLEntry.class);
1028 parser.map("collection", XMLCollection.class);
1029 parser.startWithValidation(in,
1030 "http://josm.openstreetmap.de/preferences-1.0", "resource://data/preferences.xsd");
1031 LinkedList<String> vals = new LinkedList<String>();
1032 while(parser.hasNext()) {
1033 Object o = parser.next();
1034 if(o instanceof XMLTag) {
1035 properties.put(((XMLTag)o).key, ((XMLTag)o).value);
1036 } else if (o instanceof XMLEntry) {
1037 vals.add(((XMLEntry)o).value);
1038 } else if (o instanceof XMLCollection) {
1039 properties.put(((XMLCollection)o).key, Utils.join("\u001e", vals));
1040 vals = new LinkedList<String>();
1041 }
1042 }
1043 }
1044
1045 public String toXML(boolean nopass) {
1046 StringBuilder b = new StringBuilder(
1047 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
1048 "<preferences xmlns=\"http://josm.openstreetmap.de/preferences-1.0\">\n");
1049 for (Entry<String, String> p : properties.entrySet()) {
1050 if (nopass && p.getKey().equals("osm-server.password")) {
1051 continue; // do not store plain password.
1052 }
1053 String r = p.getValue();
1054 if(r.contains("\u001e"))
1055 {
1056 b.append(" <collection key='");
1057 b.append(XmlWriter.encode(p.getKey()));
1058 b.append("'>\n");
1059 for (String val : r.split("\u001e", -1))
1060 {
1061 b.append(" <entry value='");
1062 b.append(XmlWriter.encode(val));
1063 b.append("' />\n");
1064 }
1065 b.append(" </collection>\n");
1066 }
1067 else
1068 {
1069 b.append(" <tag key='");
1070 b.append(XmlWriter.encode(p.getKey()));
1071 b.append("' value='");
1072 b.append(XmlWriter.encode(p.getValue()));
1073 b.append("' />\n");
1074 }
1075 }
1076 b.append("</preferences>");
1077 return b.toString();
1078 }
1079}
Note: See TracBrowser for help on using the repository browser.