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

Last change on this file since 3720 was 3710, checked in by bastiK, 13 years ago

new list of recently opened files; little preference rework

  • Property svn:eol-style set to native
File size: 27.2 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.nio.channels.FileChannel;
16import java.util.ArrayList;
17import java.util.Arrays;
18import java.util.Collection;
19import java.util.Collections;
20import java.util.LinkedList;
21import java.util.List;
22import java.util.Map;
23import java.util.Properties;
24import java.util.SortedMap;
25import java.util.TreeMap;
26import java.util.Map.Entry;
27import java.util.concurrent.CopyOnWriteArrayList;
28
29import javax.swing.JOptionPane;
30
31import org.openstreetmap.josm.Main;
32import org.openstreetmap.josm.tools.ColorHelper;
33
34/**
35 * This class holds all preferences for JOSM.
36 *
37 * Other classes can register their beloved properties here. All properties will be
38 * saved upon set-access.
39 *
40 * Each property is a simple key=value pair of Strings.
41 * In addition, each key has a unique default value that is set when the value is first
42 * accessed using one of the get...() methods. You can use the same preference
43 * key in different parts of the code, but the default value must be the same
44 * everywhere. null is a legitimate default value.
45 *
46 * At the moment, there is no such thing as an empty value.
47 * If you put "" or null as value, the property is removed.
48 *
49 * @author imi
50 */
51public class Preferences {
52 //static private final Logger logger = Logger.getLogger(Preferences.class.getName());
53
54 /**
55 * Internal storage for the preference directory.
56 * Do not access this variable directly!
57 * @see #getPreferencesDirFile()
58 */
59 private File preferencesDirFile = null;
60
61 /**
62 * Map the property name to the property object. Does not contain null or "" values.
63 */
64 protected final SortedMap<String, String> properties = new TreeMap<String, String>();
65 protected final SortedMap<String, String> defaults = new TreeMap<String, String>();
66
67 public interface PreferenceChangeEvent{
68 String getKey();
69 String getOldValue();
70 String getNewValue();
71 }
72
73 public interface PreferenceChangedListener {
74 void preferenceChanged(PreferenceChangeEvent e);
75 }
76
77 private static class DefaultPreferenceChangeEvent implements PreferenceChangeEvent {
78 private final String key;
79 private final String oldValue;
80 private final String newValue;
81
82 public DefaultPreferenceChangeEvent(String key, String oldValue, String newValue) {
83 this.key = key;
84 this.oldValue = oldValue;
85 this.newValue = newValue;
86 }
87
88 public String getKey() {
89 return key;
90 }
91 public String getOldValue() {
92 return oldValue;
93 }
94 public String getNewValue() {
95 return newValue;
96 }
97 }
98
99 public interface ColorKey {
100 String getColorName();
101 String getSpecialName();
102 Color getDefault();
103 }
104
105 private final CopyOnWriteArrayList<PreferenceChangedListener> listeners = new CopyOnWriteArrayList<PreferenceChangedListener>();
106
107 public void addPreferenceChangeListener(PreferenceChangedListener listener) {
108 if (listener != null) {
109 listeners.addIfAbsent(listener);
110 }
111 }
112
113 public void removePreferenceChangeListener(PreferenceChangedListener listener) {
114 listeners.remove(listener);
115 }
116
117 protected void firePreferenceChanged(String key, String oldValue, String newValue) {
118 PreferenceChangeEvent evt = new DefaultPreferenceChangeEvent(key, oldValue, newValue);
119 for (PreferenceChangedListener l : listeners) {
120 l.preferenceChanged(evt);
121 }
122 }
123
124 /**
125 * Return the location of the user defined preferences file
126 */
127 public String getPreferencesDir() {
128 final String path = getPreferencesDirFile().getPath();
129 if (path.endsWith(File.separator))
130 return path;
131 return path + File.separator;
132 }
133
134 public File getPreferencesDirFile() {
135 if (preferencesDirFile != null)
136 return preferencesDirFile;
137 String path;
138 path = System.getProperty("josm.home");
139 if (path != null) {
140 preferencesDirFile = new File(path);
141 } else {
142 path = System.getenv("APPDATA");
143 if (path != null) {
144 preferencesDirFile = new File(path, "JOSM");
145 } else {
146 preferencesDirFile = new File(System.getProperty("user.home"), ".josm");
147 }
148 }
149 return preferencesDirFile;
150 }
151
152 public File getPreferenceFile() {
153 return new File(getPreferencesDirFile(), "preferences");
154 }
155
156 public File getPluginsDirectory() {
157 return new File(getPreferencesDirFile(), "plugins");
158 }
159
160 /**
161 * @return A list of all existing directories where resources could be stored.
162 */
163 public Collection<String> getAllPossiblePreferenceDirs() {
164 LinkedList<String> locations = new LinkedList<String>();
165 locations.add(Main.pref.getPreferencesDir());
166 String s;
167 if ((s = System.getenv("JOSM_RESOURCES")) != null) {
168 if (!s.endsWith(File.separator)) {
169 s = s + File.separator;
170 }
171 locations.add(s);
172 }
173 if ((s = System.getProperty("josm.resources")) != null) {
174 if (!s.endsWith(File.separator)) {
175 s = s + File.separator;
176 }
177 locations.add(s);
178 }
179 String appdata = System.getenv("APPDATA");
180 if (System.getenv("ALLUSERSPROFILE") != null && appdata != null
181 && appdata.lastIndexOf(File.separator) != -1) {
182 appdata = appdata.substring(appdata.lastIndexOf(File.separator));
183 locations.add(new File(new File(System.getenv("ALLUSERSPROFILE"),
184 appdata), "JOSM").getPath());
185 }
186 locations.add("/usr/local/share/josm/");
187 locations.add("/usr/local/lib/josm/");
188 locations.add("/usr/share/josm/");
189 locations.add("/usr/lib/josm/");
190 return locations;
191 }
192
193 synchronized public boolean hasKey(final String key) {
194 return properties.containsKey(key);
195 }
196
197 /**
198 * Get settings value for a certain key.
199 * @param key the identifier for the setting
200 * @return "" if there is nothing set for the preference key,
201 * the corresponding value otherwise. The result is not null.
202 */
203 synchronized public String get(final String key) {
204 putDefault(key, null);
205 if (!properties.containsKey(key))
206 return "";
207 return properties.get(key);
208 }
209
210 /**
211 * Get settings value for a certain key and provide default a value.
212 * @param key the identifier for the setting
213 * @param def the default value. For each call of get() with a given key, the
214 * default value must be the same.
215 * @return the corresponding value if the property has been set before,
216 * def otherwise
217 */
218 synchronized public String get(final String key, final String def) {
219 putDefault(key, def);
220 final String prop = properties.get(key);
221 if (prop == null || prop.equals(""))
222 return def;
223 return prop;
224 }
225
226 synchronized public Map<String, String> getAllPrefix(final String prefix) {
227 final Map<String,String> all = new TreeMap<String,String>();
228 for (final Entry<String,String> e : properties.entrySet()) {
229 if (e.getKey().startsWith(prefix)) {
230 all.put(e.getKey(), e.getValue());
231 }
232 }
233 return all;
234 }
235
236 synchronized private Map<String, String> getAllPrefixDefault(final String prefix) {
237 final Map<String,String> all = new TreeMap<String,String>();
238 for (final Entry<String,String> e : defaults.entrySet()) {
239 if (e.getKey().startsWith(prefix)) {
240 all.put(e.getKey(), e.getValue());
241 }
242 }
243 return all;
244 }
245
246 synchronized public TreeMap<String, String> getAllColors() {
247 final TreeMap<String,String> all = new TreeMap<String,String>();
248 for (final Entry<String,String> e : defaults.entrySet()) {
249 if (e.getKey().startsWith("color.") && e.getValue() != null) {
250 all.put(e.getKey().substring(6), e.getValue());
251 }
252 }
253 for (final Entry<String,String> e : properties.entrySet()) {
254 if (e.getKey().startsWith("color.")) {
255 all.put(e.getKey().substring(6), e.getValue());
256 }
257 }
258 return all;
259 }
260
261 synchronized public Map<String, String> getDefaults() {
262 return defaults;
263 }
264
265 synchronized public void putDefault(final String key, final String def) {
266 if(!defaults.containsKey(key) || defaults.get(key) == null) {
267 defaults.put(key, def);
268 } else if(def != null && !defaults.get(key).equals(def)) {
269 System.out.println("Defaults for " + key + " differ: " + def + " != " + defaults.get(key));
270 }
271 }
272
273 synchronized public boolean getBoolean(final String key) {
274 putDefault(key, null);
275 return properties.containsKey(key) ? Boolean.parseBoolean(properties.get(key)) : false;
276 }
277
278 synchronized public boolean getBoolean(final String key, final boolean def) {
279 putDefault(key, Boolean.toString(def));
280 return properties.containsKey(key) ? Boolean.parseBoolean(properties.get(key)) : def;
281 }
282
283 /**
284 * Set a value for a certain setting. The changed setting is saved
285 * to the preference file immediately. Due to caching mechanisms on modern
286 * operating systems and hardware, this shouldn't be a performance problem.
287 * @param key the unique identifier for the setting
288 * @param value the value of the setting. Can be null or "" wich both removes
289 * the key-value entry.
290 * @return if true, something has changed (i.e. value is different than before)
291 */
292 public boolean put(final String key, String value) {
293 boolean changed = false;
294 String oldValue = null;
295
296 synchronized (this) {
297 oldValue = properties.get(key);
298 if(value != null && value.length() == 0) {
299 value = null;
300 }
301 // value is the same as before - no need to save anything
302 boolean equalValue = oldValue != null && oldValue.equals(value);
303 // The setting was previously unset and we are supposed to put a
304 // value that equals the default value. This is not necessary because
305 // the default value is the same throughout josm. In addition we like
306 // to have the possibility to change the default value from version
307 // to version, which would not work if we wrote it to the preference file.
308 boolean unsetIsDefault = oldValue == null && (value == null || value.equals(defaults.get(key)));
309
310 if (!(equalValue || unsetIsDefault)) {
311 if (value == null) {
312 properties.remove(key);
313 } else {
314 properties.put(key, value);
315 }
316 try {
317 save();
318 } catch(IOException e){
319 System.out.println(tr("Warning: failed to persist preferences to ''{0}''", getPreferenceFile().getAbsoluteFile()));
320 }
321 changed = true;
322 }
323 }
324 if (changed) {
325 // Call outside of synchronized section in case some listener wait for other thread that wait for preference lock
326 firePreferenceChanged(key, oldValue, value);
327 }
328 return changed;
329 }
330
331 public boolean put(final String key, final boolean value) {
332 return put(key, Boolean.toString(value));
333 }
334
335 public boolean putInteger(final String key, final Integer value) {
336 return put(key, Integer.toString(value));
337 }
338
339 public boolean putDouble(final String key, final Double value) {
340 return put(key, Double.toString(value));
341 }
342
343 public boolean putLong(final String key, final Long value) {
344 return put(key, Long.toString(value));
345 }
346
347 /**
348 * Called after every put. In case of a problem, do nothing but output the error
349 * in log.
350 */
351 public void save() throws IOException {
352 /* currently unused, but may help to fix configuration issues in future */
353 putInteger("josm.version", Version.getInstance().getVersion());
354
355 updateSystemProperties();
356 if(Main.applet)
357 return;
358 File prefFile = new File(getPreferencesDirFile(), "preferences");
359
360 // Backup old preferences if there are old preferences
361 if(prefFile.exists()) {
362 copyFile(prefFile, new File(prefFile + "_backup"));
363 }
364
365 final PrintWriter out = new PrintWriter(new OutputStreamWriter(
366 new FileOutputStream(prefFile + "_tmp"), "utf-8"), false);
367 for (final Entry<String, String> e : properties.entrySet()) {
368 String s = defaults.get(e.getKey());
369 /* don't save default values */
370 if(s == null || !s.equals(e.getValue())) {
371 out.println(e.getKey() + "=" + e.getValue());
372 }
373 }
374 out.close();
375
376 File tmpFile = new File(prefFile + "_tmp");
377 copyFile(tmpFile, prefFile);
378 tmpFile.delete();
379 }
380
381 /**
382 * Simple file copy function that will overwrite the target file
383 * Taken from http://www.rgagnon.com/javadetails/java-0064.html (CC-NC-BY-SA)
384 * @param in
385 * @param out
386 * @throws IOException
387 */
388 public static void copyFile(File in, File out) throws IOException {
389 FileChannel inChannel = new FileInputStream(in).getChannel();
390 FileChannel outChannel = new FileOutputStream(out).getChannel();
391 try {
392 inChannel.transferTo(0, inChannel.size(),
393 outChannel);
394 }
395 catch (IOException e) {
396 throw e;
397 }
398 finally {
399 if (inChannel != null) {
400 inChannel.close();
401 }
402 if (outChannel != null) {
403 outChannel.close();
404 }
405 }
406 }
407
408 public void load() throws IOException {
409 properties.clear();
410 if(!Main.applet) {
411 final BufferedReader in = new BufferedReader(new InputStreamReader(
412 new FileInputStream(getPreferencesDir()+"preferences"), "utf-8"));
413 int lineNumber = 0;
414 ArrayList<Integer> errLines = new ArrayList<Integer>();
415 for (String line = in.readLine(); line != null; line = in.readLine(), lineNumber++) {
416 final int i = line.indexOf('=');
417 if (i == -1 || i == 0) {
418 errLines.add(lineNumber);
419 continue;
420 }
421 String key = line.substring(0,i);
422 String value = line.substring(i+1);
423 if (!value.isEmpty()) {
424 properties.put(key, value);
425 }
426 }
427 if (!errLines.isEmpty())
428 throw new IOException(tr("Malformed config file at lines {0}", errLines));
429 }
430 updateSystemProperties();
431 }
432
433 public void init(boolean reset){
434 if(Main.applet)
435 return;
436 // get the preferences.
437 File prefDir = getPreferencesDirFile();
438 if (prefDir.exists()) {
439 if(!prefDir.isDirectory()) {
440 System.err.println(tr("Warning: Failed to initialize preferences. Preference directory ''{0}'' is not a directory.", prefDir.getAbsoluteFile()));
441 JOptionPane.showMessageDialog(
442 Main.parent,
443 tr("<html>Failed to initialize preferences.<br>Preference directory ''{0}'' is not a directory.</html>", prefDir.getAbsoluteFile()),
444 tr("Error"),
445 JOptionPane.ERROR_MESSAGE
446 );
447 return;
448 }
449 } else {
450 if (! prefDir.mkdirs()) {
451 System.err.println(tr("Warning: Failed to initialize preferences. Failed to create missing preference directory: {0}", prefDir.getAbsoluteFile()));
452 JOptionPane.showMessageDialog(
453 Main.parent,
454 tr("<html>Failed to initialize preferences.<br>Failed to create missing preference directory: {0}</html>",prefDir.getAbsoluteFile()),
455 tr("Error"),
456 JOptionPane.ERROR_MESSAGE
457 );
458 return;
459 }
460 }
461
462 File preferenceFile = getPreferenceFile();
463 try {
464 if (!preferenceFile.exists()) {
465 System.out.println(tr("Warning: Missing preference file ''{0}''. Creating a default preference file.", preferenceFile.getAbsoluteFile()));
466 resetToDefault();
467 save();
468 } else if (reset) {
469 System.out.println(tr("Warning: Replacing existing preference file ''{0}'' with default preference file.", preferenceFile.getAbsoluteFile()));
470 resetToDefault();
471 save();
472 }
473 } catch(IOException e) {
474 e.printStackTrace();
475 JOptionPane.showMessageDialog(
476 Main.parent,
477 tr("<html>Failed to initialize preferences.<br>Failed to reset preference file to default: {0}</html>",getPreferenceFile().getAbsoluteFile()),
478 tr("Error"),
479 JOptionPane.ERROR_MESSAGE
480 );
481 return;
482 }
483 try {
484 load();
485 } catch (IOException e) {
486 e.printStackTrace();
487 File backupFile = new File(prefDir,"preferences.bak");
488 JOptionPane.showMessageDialog(
489 Main.parent,
490 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()),
491 tr("Error"),
492 JOptionPane.ERROR_MESSAGE
493 );
494 preferenceFile.renameTo(backupFile);
495 try {
496 resetToDefault();
497 save();
498 } catch(IOException e1) {
499 e1.printStackTrace();
500 System.err.println(tr("Warning: Failed to initialize preferences.Failed to reset preference file to default: {0}", getPreferenceFile()));
501 }
502 }
503 }
504
505 public final void resetToDefault(){
506 properties.clear();
507 }
508
509 /**
510 * Convenience method for accessing colour preferences.
511 *
512 * @param colName name of the colour
513 * @param def default value
514 * @return a Color object for the configured colour, or the default value if none configured.
515 */
516 synchronized public Color getColor(String colName, Color def) {
517 return getColor(colName, null, def);
518 }
519
520 public Color getColor(ColorKey key) {
521 return getColor(key.getColorName(), key.getSpecialName(), key.getDefault());
522 }
523
524 /**
525 * Convenience method for accessing colour preferences.
526 *
527 * @param colName name of the colour
528 * @param specName name of the special colour settings
529 * @param def default value
530 * @return a Color object for the configured colour, or the default value if none configured.
531 */
532 synchronized public Color getColor(String colName, String specName, Color def) {
533 putDefault("color."+colName, ColorHelper.color2html(def));
534 String colStr = specName != null ? get("color."+specName) : "";
535 if(colStr.equals("")) {
536 colStr = get("color."+colName);
537 }
538 return colStr.equals("") ? def : ColorHelper.html2color(colStr);
539 }
540
541 synchronized public Color getDefaultColor(String colName) {
542 String colStr = defaults.get("color."+colName);
543 return colStr == null || "".equals(colStr) ? null : ColorHelper.html2color(colStr);
544 }
545
546 synchronized public boolean putColor(String colName, Color val) {
547 return put("color."+colName, val != null ? ColorHelper.color2html(val) : null);
548 }
549
550 synchronized public int getInteger(String key, int def) {
551 putDefault(key, Integer.toString(def));
552 String v = get(key);
553 if(null == v)
554 return def;
555
556 try {
557 return Integer.parseInt(v);
558 } catch(NumberFormatException e) {
559 // fall out
560 }
561 return def;
562 }
563
564 synchronized public long getLong(String key, long def) {
565 putDefault(key, Long.toString(def));
566 String v = get(key);
567 if(null == v)
568 return def;
569
570 try {
571 return Long.parseLong(v);
572 } catch(NumberFormatException e) {
573 // fall out
574 }
575 return def;
576 }
577
578 synchronized public double getDouble(String key, double def) {
579 putDefault(key, Double.toString(def));
580 String v = get(key);
581 if(null == v)
582 return def;
583
584 try {
585 return Double.parseDouble(v);
586 } catch(NumberFormatException e) {
587 // fall out
588 }
589 return def;
590 }
591
592 synchronized public double getDouble(String key, String def) {
593 putDefault(key, def);
594 String v = get(key);
595 if(v != null && v.length() != 0) {
596 try { return Double.parseDouble(v); } catch(NumberFormatException e) {}
597 }
598 try { return Double.parseDouble(def); } catch(NumberFormatException e) {}
599 return 0.0;
600 }
601
602 synchronized public String getCollectionAsString(final String key) {
603 String s = get(key);
604 if(s != null && s.length() != 0) {
605 s = s.replaceAll("\u001e",",");
606 }
607 return s;
608 }
609
610 public boolean isCollection(String key, boolean def) {
611 String s = get(key);
612 if (s != null && s.length() != 0)
613 return s.indexOf("\u001e") >= 0;
614 else
615 return def;
616 }
617
618 /**
619 * Get a list of values for a certain key
620 * @param key the identifier for the setting
621 * @param def the default value.
622 * @return the corresponding value if the property has been set before,
623 * def otherwise
624 */
625 synchronized public Collection<String> getCollection(String key, Collection<String> def) {
626 putCollectionDefault(key, def);
627 String s = get(key);
628 if(s != null && s.length() != 0)
629 return Arrays.asList(s.split("\u001e"));
630 return def;
631 }
632
633 /**
634 * Get a list of values for a certain key
635 * @param key the identifier for the setting
636 * @return the corresponding value if the property has been set before,
637 * an empty Collection otherwise.
638 */
639 synchronized public Collection<String> getCollection(String key) {
640 putCollectionDefault(key, null);
641 String s = get(key);
642 if (s != null && s.length() != 0)
643 return Arrays.asList(s.split("\u001e"));
644 return Collections.emptyList();
645 }
646
647 synchronized public void removeFromCollection(String key, String value) {
648 List<String> a = new ArrayList<String>(getCollection(key, Collections.<String>emptyList()));
649 a.remove(value);
650 putCollection(key, a);
651 }
652
653 synchronized public boolean putCollection(String key, Collection<String> val) {
654 return put(key, join("\u001e", val));
655 }
656
657 synchronized private void putCollectionDefault(String key, Collection<String> val) {
658 putDefault(key, join("\u001e", val));
659 }
660
661 /**
662 * Used to read a 2-dimensional array of strings from the preference file.
663 * If not a single entry could be found, def is returned.
664 */
665 synchronized public Collection<Collection<String>> getArray(String key,
666 Collection<Collection<String>> def)
667 {
668 if(def != null)
669 putArrayDefault(key, def);
670 key += ".";
671 int num = 0;
672 Collection<Collection<String>> col = new LinkedList<Collection<String>>();
673 while(properties.containsKey(key+num)) {
674 col.add(getCollection(key+num++, null));
675 }
676 return num == 0 ? def : col;
677 }
678
679 synchronized public boolean putArray(String key, Collection<Collection<String>> val) {
680 boolean changed = false;
681 key += ".";
682 Collection<String> keys = getAllPrefix(key).keySet();
683 if(val != null) {
684 int num = 0;
685 for(Collection<String> c : val) {
686 keys.remove(key+num);
687 changed |= putCollection(key+num++, c);
688 }
689 }
690 int l = key.length();
691 for(String k : keys) {
692 try {
693 Integer.valueOf(k.substring(l));
694 changed |= put(k, null);
695 } catch(NumberFormatException e) {
696 /* everything which does not end with a number should not be deleted */
697 }
698 }
699 return changed;
700 }
701
702 synchronized private void putArrayDefault(String key, Collection<Collection<String>> val) {
703 key += ".";
704 Collection<String> keys = getAllPrefixDefault(key).keySet();
705 int num = 0;
706 for(Collection<String> c : val) {
707 keys.remove(key+num);
708 putCollectionDefault(key+num++, c);
709 }
710 int l = key.length();
711 for(String k : keys) {
712 try {
713 Integer.valueOf(k.substring(l));
714 defaults.remove(k);
715 } catch(Exception e) {
716 /* everything which does not end with a number should not be deleted */
717 }
718 }
719 }
720
721 /**
722 * Updates system properties with the current values in the preferences.
723 *
724 */
725 public void updateSystemProperties() {
726 Properties sysProp = System.getProperties();
727 sysProp.put("http.agent", Version.getInstance().getAgentString());
728 System.setProperties(sysProp);
729 }
730
731 /**
732 * The default plugin site
733 */
734 private final static String[] DEFAULT_PLUGIN_SITE = {"http://josm.openstreetmap.de/plugin%<?plugins=>"};
735
736 /**
737 * Replies the collection of plugin site URLs from where plugin lists can be downloaded
738 *
739 * @return
740 */
741 public Collection<String> getPluginSites() {
742 return getCollection("pluginmanager.sites", Arrays.asList(DEFAULT_PLUGIN_SITE));
743 }
744
745 /**
746 * Sets the collection of plugin site URLs.
747 *
748 * @param sites the site URLs
749 */
750 public void setPluginSites(Collection<String> sites) {
751 putCollection("pluginmanager.sites", sites);
752 }
753
754 /**
755 * Joins a collection of strings into a single string with fields
756 * separated by the value of sep.
757 * @param sep the separator
758 * @param values collection of strings, null strings are converted to the
759 * empty string
760 * @return null if values is null. The joined string otherwise.
761 */
762 public static String join(String sep, Collection<?> values) {
763 if (values == null)
764 return null;
765 if (values.isEmpty())
766 return "";
767 StringBuilder s = null;
768 for (Object a : values) {
769 if (a == null) {
770 a = "";
771 }
772 if(s != null) {
773 s.append(sep).append(a.toString());
774 } else {
775 s = new StringBuilder(a.toString());
776 }
777 }
778 return s.toString();
779 }
780
781}
Note: See TracBrowser for help on using the repository browser.