fix bundles reset to default
[nikiroo-utils.git] / src / be / nikiroo / utils / resources / Bundle.java
1 package be.nikiroo.utils.resources;
2
3 import java.io.BufferedWriter;
4 import java.io.File;
5 import java.io.FileInputStream;
6 import java.io.FileOutputStream;
7 import java.io.IOException;
8 import java.io.InputStreamReader;
9 import java.io.OutputStreamWriter;
10 import java.io.Reader;
11 import java.io.Writer;
12 import java.lang.reflect.Field;
13 import java.util.ArrayList;
14 import java.util.HashMap;
15 import java.util.List;
16 import java.util.Locale;
17 import java.util.Map;
18 import java.util.MissingResourceException;
19 import java.util.PropertyResourceBundle;
20 import java.util.ResourceBundle;
21
22 import be.nikiroo.utils.resources.Meta.Format;
23
24 /**
25 * This class encapsulate a {@link ResourceBundle} in UTF-8. It allows to
26 * retrieve values associated to an enumeration, and allows some additional
27 * methods.
28 * <p>
29 * It also sports a writable change map, and you can save back the
30 * {@link Bundle} to file with {@link Bundle#updateFile(String)}.
31 *
32 * @param <E>
33 * the enum to use to get values out of this class
34 *
35 * @author niki
36 */
37
38 public class Bundle<E extends Enum<E>> {
39 /** The type of E. */
40 protected Class<E> type;
41 /**
42 * The {@link Enum} associated to this {@link Bundle} (all the keys used in
43 * this {@link Bundle} will be of this type).
44 */
45 protected Enum<?> keyType;
46
47 private TransBundle<E> descriptionBundle;
48
49 /** R/O map */
50 private Map<String, String> map;
51 /** R/W map */
52 private Map<String, String> changeMap;
53
54 /**
55 * Create a new {@link Bundles} of the given name.
56 *
57 * @param type
58 * a runtime instance of the class of E
59 * @param name
60 * the name of the {@link Bundles}
61 * @param descriptionBundle
62 * the description {@link TransBundle}, that is, a
63 * {@link TransBundle} dedicated to the description of the values
64 * of the given {@link Bundle} (can be NULL)
65 */
66 protected Bundle(Class<E> type, Enum<?> name,
67 TransBundle<E> descriptionBundle) {
68 this.type = type;
69 this.keyType = name;
70 this.descriptionBundle = descriptionBundle;
71
72 this.map = new HashMap<String, String>();
73 this.changeMap = new HashMap<String, String>();
74 setBundle(name, Locale.getDefault(), false);
75 }
76
77 /**
78 * Check if the setting is set into this {@link Bundle}.
79 *
80 * @param id
81 * the id of the setting to check
82 * @param includeDefaultValue
83 * TRUE to only return false when the setting is not set AND
84 * there is no default value
85 *
86 * @return TRUE if the setting is set
87 */
88 public boolean isSet(E id, boolean includeDefaultValue) {
89 return isSet(id.name(), includeDefaultValue);
90 }
91
92 /**
93 * Check if the setting is set into this {@link Bundle}.
94 *
95 * @param id
96 * the id of the setting to check
97 * @param includeDefaultValue
98 * TRUE to only return false when the setting is not set AND
99 * there is no default value
100 *
101 * @return TRUE if the setting is set
102 */
103 protected boolean isSet(String name, boolean includeDefaultValue) {
104 if (getString(name, null) == null) {
105 if (!includeDefaultValue || getString(name, "") == null) {
106 return false;
107 }
108 }
109
110 return true;
111 }
112
113 /**
114 * Return the value associated to the given id as a {@link String}.
115 *
116 * @param id
117 * the id of the value to get
118 *
119 * @return the associated value, or NULL if not found (not present in the
120 * resource file)
121 */
122 public String getString(E id) {
123 return getString(id, null);
124 }
125
126 /**
127 * Return the value associated to the given id as a {@link String}.
128 * <p>
129 * If no value is associated, take the default one if any.
130 *
131 * @param id
132 * the id of the value to get
133 * @param def
134 * the default value when it is not present in the config file
135 *
136 * @return the associated value, or NULL if not found (not present in the
137 * resource file)
138 */
139 public String getString(E id, String def) {
140 return getString(id, def, -1);
141 }
142
143 /**
144 * Return the value associated to the given id as a {@link String}.
145 * <p>
146 * If no value is associated (or if it is empty!), take the default one if
147 * any.
148 *
149 * @param id
150 * the id of the value to get
151 * @param def
152 * the default value when it is not present in the config file
153 * @param item
154 * the item number to get for an array of values, or -1 for
155 * non-arrays
156 *
157 * @return the associated value, or NULL if not found (not present in the
158 * resource file)
159 */
160 public String getString(E id, String def, int item) {
161 String rep = getString(id.name(), null);
162 if (rep == null) {
163 try {
164 Meta meta = type.getDeclaredField(id.name()).getAnnotation(
165 Meta.class);
166 rep = meta.def();
167 } catch (NoSuchFieldException e) {
168 } catch (SecurityException e) {
169 }
170 }
171
172 if (rep == null || rep.isEmpty()) {
173 return def;
174 }
175
176 if (item >= 0) {
177 List<String> values = BundleHelper.parseList(rep, item);
178 if (values != null && item < values.size()) {
179 return values.get(item);
180 }
181
182 return null;
183 }
184
185 return rep;
186 }
187
188 /**
189 * Set the value associated to the given id as a {@link String}.
190 *
191 * @param id
192 * the id of the value to set
193 * @param value
194 * the value
195 *
196 */
197 public void setString(E id, String value) {
198 setString(id.name(), value);
199 }
200
201 /**
202 * Set the value associated to the given id as a {@link String}.
203 *
204 * @param id
205 * the id of the value to set
206 * @param value
207 * the value
208 * @param item
209 * the item number to get for an array of values, or -1 for
210 * non-arrays
211 *
212 */
213 public void setString(E id, String value, int item) {
214 if (item < 0) {
215 setString(id.name(), value);
216 } else {
217 List<String> values = getList(id);
218 for (int i = values.size(); i < item; i++) {
219 values.add(null);
220 }
221 values.set(item, value);
222 setString(id.name(), BundleHelper.fromList(values));
223 }
224 }
225
226 /**
227 * Return the value associated to the given id as a {@link String} suffixed
228 * with the runtime value "_suffix" (that is, "_" and suffix).
229 * <p>
230 * Will only accept suffixes that form an existing id.
231 * <p>
232 * If no value is associated, take the default one if any.
233 *
234 * @param id
235 * the id of the value to get
236 * @param suffix
237 * the runtime suffix
238 *
239 * @return the associated value, or NULL if not found (not present in the
240 * resource file)
241 */
242 public String getStringX(E id, String suffix) {
243 return getStringX(id, suffix, null, -1);
244 }
245
246 /**
247 * Return the value associated to the given id as a {@link String} suffixed
248 * with the runtime value "_suffix" (that is, "_" and suffix).
249 * <p>
250 * Will only accept suffixes that form an existing id.
251 * <p>
252 * If no value is associated, take the default one if any.
253 *
254 * @param id
255 * the id of the value to get
256 * @param suffix
257 * the runtime suffix
258 * @param def
259 * the default value when it is not present in the config file
260 *
261 * @return the associated value, or NULL if not found (not present in the
262 * resource file)
263 */
264 public String getStringX(E id, String suffix, String def) {
265 return getStringX(id, suffix, def, -1);
266 }
267
268 /**
269 * Return the value associated to the given id as a {@link String} suffixed
270 * with the runtime value "_suffix" (that is, "_" and suffix).
271 * <p>
272 * Will only accept suffixes that form an existing id.
273 * <p>
274 * If no value is associated, take the default one if any.
275 *
276 * @param id
277 * the id of the value to get
278 * @param suffix
279 * the runtime suffix
280 * @param item
281 * the item number to get for an array of values, or -1 for
282 * non-arrays
283 * @param def
284 * the default value when it is not present in the config file
285 * @param item
286 * the item number to get for an array of values, or -1 for
287 * non-arrays
288 *
289 * @return the associated value, or NULL if not found (not present in the
290 * resource file)
291 */
292 public String getStringX(E id, String suffix, String def, int item) {
293 String key = id.name()
294 + (suffix == null ? "" : "_" + suffix.toUpperCase());
295
296 try {
297 id = Enum.valueOf(type, key);
298 return getString(id, def, item);
299 } catch (IllegalArgumentException e) {
300 }
301
302 return null;
303 }
304
305 /**
306 * Set the value associated to the given id as a {@link String} suffixed
307 * with the runtime value "_suffix" (that is, "_" and suffix).
308 * <p>
309 * Will only accept suffixes that form an existing id.
310 *
311 * @param id
312 * the id of the value to set
313 * @param suffix
314 * the runtime suffix
315 * @param value
316 * the value
317 */
318 public void setStringX(E id, String suffix, String value) {
319 setStringX(id, suffix, value, -1);
320 }
321
322 /**
323 * Set the value associated to the given id as a {@link String} suffixed
324 * with the runtime value "_suffix" (that is, "_" and suffix).
325 * <p>
326 * Will only accept suffixes that form an existing id.
327 *
328 * @param id
329 * the id of the value to set
330 * @param suffix
331 * the runtime suffix
332 * @param value
333 * the value
334 * @param item
335 * the item number to get for an array of values, or -1 for
336 * non-arrays
337 */
338 public void setStringX(E id, String suffix, String value, int item) {
339 String key = id.name()
340 + (suffix == null ? "" : "_" + suffix.toUpperCase());
341
342 try {
343 id = Enum.valueOf(type, key);
344 setString(id, value, item);
345 } catch (IllegalArgumentException e) {
346 }
347 }
348
349 /**
350 * Return the value associated to the given id as a {@link Boolean}.
351 * <p>
352 * If no value is associated, take the default one if any.
353 *
354 * @param id
355 * the id of the value to get
356 *
357 * @return the associated value
358 */
359 public Boolean getBoolean(E id) {
360 return BundleHelper.parseBoolean(getString(id), -1);
361 }
362
363 /**
364 * Return the value associated to the given id as a {@link Boolean}.
365 * <p>
366 * If no value is associated, take the default one if any.
367 *
368 * @param id
369 * the id of the value to get
370 * @param def
371 * the default value when it is not present in the config file or
372 * if it is not a boolean value
373 *
374 * @return the associated value
375 */
376 public boolean getBoolean(E id, boolean def) {
377 Boolean value = getBoolean(id);
378 if (value != null) {
379 return value;
380 }
381
382 return def;
383 }
384
385 /**
386 * Return the value associated to the given id as a {@link Boolean}.
387 * <p>
388 * If no value is associated, take the default one if any.
389 *
390 * @param id
391 * the id of the value to get
392 * @param def
393 * the default value when it is not present in the config file or
394 * if it is not a boolean value
395 * @param item
396 * the item number to get for an array of values, or -1 for
397 * non-arrays
398 *
399 * @return the associated value
400 */
401 public Boolean getBoolean(E id, boolean def, int item) {
402 String value = getString(id);
403 if (value != null) {
404 return BundleHelper.parseBoolean(value, item);
405 }
406
407 return def;
408 }
409
410 /**
411 * Set the value associated to the given id as a {@link Boolean}.
412 *
413 * @param id
414 * the id of the value to set
415 * @param value
416 * the value
417 *
418 */
419 public void setBoolean(E id, boolean value) {
420 setBoolean(id, value, -1);
421 }
422
423 /**
424 * Set the value associated to the given id as a {@link Boolean}.
425 *
426 * @param id
427 * the id of the value to set
428 * @param value
429 * the value
430 * @param item
431 * the item number to get for an array of values, or -1 for
432 * non-arrays
433 *
434 */
435 public void setBoolean(E id, boolean value, int item) {
436 setString(id, BundleHelper.fromBoolean(value), item);
437 }
438
439 /**
440 * Return the value associated to the given id as an {@link Integer}.
441 * <p>
442 * If no value is associated, take the default one if any.
443 *
444 * @param id
445 * the id of the value to get
446 *
447 * @return the associated value
448 */
449 public Integer getInteger(E id) {
450 String value = getString(id);
451 if (value != null) {
452 return BundleHelper.parseInteger(value, -1);
453 }
454
455 return null;
456 }
457
458 /**
459 * Return the value associated to the given id as an int.
460 * <p>
461 * If no value is associated, take the default one if any.
462 *
463 * @param id
464 * the id of the value to get
465 * @param def
466 * the default value when it is not present in the config file or
467 * if it is not a int value
468 *
469 * @return the associated value
470 */
471 public int getInteger(E id, int def) {
472 Integer value = getInteger(id);
473 if (value != null) {
474 return value;
475 }
476
477 return def;
478 }
479
480 /**
481 * Return the value associated to the given id as an int.
482 * <p>
483 * If no value is associated, take the default one if any.
484 *
485 * @param id
486 * the id of the value to get
487 * @param def
488 * the default value when it is not present in the config file or
489 * if it is not a int value
490 * @param item
491 * the item number to get for an array of values, or -1 for
492 * non-arrays
493 *
494 * @return the associated value
495 */
496 public Integer getInteger(E id, int def, int item) {
497 String value = getString(id);
498 if (value != null) {
499 return BundleHelper.parseInteger(value, item);
500 }
501
502 return def;
503 }
504
505 /**
506 * Set the value associated to the given id as a {@link Integer}.
507 *
508 * @param id
509 * the id of the value to set
510 * @param value
511 * the value
512 *
513 */
514 public void setInteger(E id, int value) {
515 setInteger(id, value, -1);
516 }
517
518 /**
519 * Set the value associated to the given id as a {@link Integer}.
520 *
521 * @param id
522 * the id of the value to set
523 * @param value
524 * the value
525 * @param item
526 * the item number to get for an array of values, or -1 for
527 * non-arrays
528 *
529 */
530 public void setInteger(E id, int value, int item) {
531 setString(id, BundleHelper.fromInteger(value), item);
532 }
533
534 /**
535 * Return the value associated to the given id as a {@link Character}.
536 * <p>
537 * If no value is associated, take the default one if any.
538 *
539 * @param id
540 * the id of the value to get
541 *
542 * @return the associated value
543 */
544 public Character getCharacter(E id) {
545 return BundleHelper.parseCharacter(getString(id), -1);
546 }
547
548 /**
549 * Return the value associated to the given id as a {@link Character}.
550 * <p>
551 * If no value is associated, take the default one if any.
552 *
553 * @param id
554 * the id of the value to get
555 * @param def
556 * the default value when it is not present in the config file or
557 * if it is not a char value
558 *
559 * @return the associated value
560 */
561 public char getCharacter(E id, char def) {
562 Character value = getCharacter(id);
563 if (value != null) {
564 return value;
565 }
566
567 return def;
568 }
569
570 /**
571 * Return the value associated to the given id as a {@link Character}.
572 * <p>
573 * If no value is associated, take the default one if any.
574 *
575 * @param id
576 * the id of the value to get
577 * @param def
578 * the default value when it is not present in the config file or
579 * if it is not a char value
580 *
581 * @return the associated value
582 */
583 public Character getCharacter(E id, char def, int item) {
584 String value = getString(id);
585 if (value != null) {
586 return BundleHelper.parseCharacter(value, item);
587 }
588
589 return def;
590 }
591
592 /**
593 * Set the value associated to the given id as a {@link Character}.
594 *
595 * @param id
596 * the id of the value to set
597 * @param value
598 * the value
599 *
600 */
601 public void setCharacter(E id, char value) {
602 setCharacter(id, value, -1);
603 }
604
605 /**
606 * Set the value associated to the given id as a {@link Character}.
607 *
608 * @param id
609 * the id of the value to set
610 * @param value
611 * the value
612 * @param item
613 * the item number to get for an array of values, or -1 for
614 * non-arrays
615 *
616 */
617 public void setCharacter(E id, char value, int item) {
618 setString(id, BundleHelper.fromCharacter(value), item);
619 }
620
621 /**
622 * Return the value associated to the given id as a colour if it is found
623 * and can be parsed.
624 * <p>
625 * The returned value is an ARGB value.
626 * <p>
627 * If no value is associated, take the default one if any.
628 *
629 * @param id
630 * the id of the value to get
631 *
632 * @return the associated value
633 */
634 public Integer getColor(E id) {
635 return BundleHelper.parseColor(getString(id), -1);
636 }
637
638 /**
639 * Return the value associated to the given id as a colour if it is found
640 * and can be parsed.
641 * <p>
642 * The returned value is an ARGB value.
643 * <p>
644 * If no value is associated, take the default one if any.
645 *
646 * @param id
647 * the id of the value to get
648 *
649 * @return the associated value
650 */
651 public int getColor(E id, int def) {
652 Integer value = getColor(id);
653 if (value != null) {
654 return value;
655 }
656
657 return def;
658 }
659
660 /**
661 * Return the value associated to the given id as a colour if it is found
662 * and can be parsed.
663 * <p>
664 * The returned value is an ARGB value.
665 * <p>
666 * If no value is associated, take the default one if any.
667 *
668 * @param id
669 * the id of the value to get
670 *
671 * @return the associated value
672 */
673 public Integer getColor(E id, int def, int item) {
674 String value = getString(id);
675 if (value != null) {
676 return BundleHelper.parseColor(value, item);
677 }
678
679 return def;
680 }
681
682 /**
683 * Set the value associated to the given id as a colour.
684 * <p>
685 * The value is a BGRA value.
686 *
687 * @param id
688 * the id of the value to set
689 * @param color
690 * the new colour
691 */
692 public void setColor(E id, Integer color) {
693 setColor(id, color, -1);
694 }
695
696 /**
697 * Set the value associated to the given id as a Color.
698 *
699 * @param id
700 * the id of the value to set
701 * @param value
702 * the value
703 * @param item
704 * the item number to get for an array of values, or -1 for
705 * non-arrays
706 *
707 */
708 public void setColor(E id, int value, int item) {
709 setString(id, BundleHelper.fromColor(value), item);
710 }
711
712 /**
713 * Return the value associated to the given id as a list of values if it is
714 * found and can be parsed.
715 * <p>
716 * If no value is associated, take the default one if any.
717 *
718 * @param id
719 * the id of the value to get
720 *
721 * @return the associated list, empty if the value is empty, NULL if it is
722 * not found or cannot be parsed as a list
723 */
724 public List<String> getList(E id) {
725 return BundleHelper.parseList(getString(id), -1);
726 }
727
728 /**
729 * Return the value associated to the given id as a list of values if it is
730 * found and can be parsed.
731 * <p>
732 * If no value is associated, take the default one if any.
733 *
734 * @param id
735 * the id of the value to get
736 *
737 * @return the associated list, empty if the value is empty, NULL if it is
738 * not found or cannot be parsed as a list
739 */
740 public List<String> getList(E id, List<String> def) {
741 List<String> value = getList(id);
742 if (value != null) {
743 return value;
744 }
745
746 return def;
747 }
748
749 /**
750 * Return the value associated to the given id as a list of values if it is
751 * found and can be parsed.
752 * <p>
753 * If no value is associated, take the default one if any.
754 *
755 * @param id
756 * the id of the value to get
757 *
758 * @return the associated list, empty if the value is empty, NULL if it is
759 * not found or cannot be parsed as a list
760 */
761 public List<String> getList(E id, List<String> def, int item) {
762 String value = getString(id);
763 if (value != null) {
764 return BundleHelper.parseList(value, item);
765 }
766
767 return def;
768 }
769
770 /**
771 * Set the value associated to the given id as a list of values.
772 *
773 * @param id
774 * the id of the value to set
775 * @param list
776 * the new list of values
777 */
778 public void setList(E id, List<String> list) {
779 setList(id, list, -1);
780 }
781
782 /**
783 * Set the value associated to the given id as a {@link List}.
784 *
785 * @param id
786 * the id of the value to set
787 * @param value
788 * the value
789 * @param item
790 * the item number to get for an array of values, or -1 for
791 * non-arrays
792 *
793 */
794 public void setList(E id, List<String> value, int item) {
795 setString(id, BundleHelper.fromList(value), item);
796 }
797
798 /**
799 * Create/update the .properties file.
800 * <p>
801 * Will use the most likely candidate as base if the file does not already
802 * exists and this resource is translatable (for instance, "en_US" will use
803 * "en" as a base if the resource is a translation file).
804 * <p>
805 * Will update the files in {@link Bundles#getDirectory()}; it <b>MUST</b>
806 * be set.
807 *
808 * @throws IOException
809 * in case of IO errors
810 */
811 public void updateFile() throws IOException {
812 updateFile(Bundles.getDirectory());
813 }
814
815 /**
816 * Create/update the .properties file.
817 * <p>
818 * Will use the most likely candidate as base if the file does not already
819 * exists and this resource is translatable (for instance, "en_US" will use
820 * "en" as a base if the resource is a translation file).
821 *
822 * @param path
823 * the path where the .properties files are, <b>MUST NOT</b> be
824 * NULL
825 *
826 * @throws IOException
827 * in case of IO errors
828 */
829 public void updateFile(String path) throws IOException {
830 File file = getUpdateFile(path);
831
832 BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(
833 new FileOutputStream(file), "UTF-8"));
834
835 writeHeader(writer);
836 writer.write("\n");
837 writer.write("\n");
838
839 for (Field field : type.getDeclaredFields()) {
840 Meta meta = field.getAnnotation(Meta.class);
841 if (meta != null) {
842 E id = Enum.valueOf(type, field.getName());
843 String info = getMetaInfo(meta);
844
845 if (info != null) {
846 writer.write(info);
847 writer.write("\n");
848 }
849
850 writeValue(writer, id);
851 }
852 }
853
854 writer.close();
855 }
856
857 /**
858 * Delete the .properties file.
859 * <p>
860 * Will use the most likely candidate as base if the file does not already
861 * exists and this resource is translatable (for instance, "en_US" will use
862 * "en" as a base if the resource is a translation file).
863 * <p>
864 * Will delete the files in {@link Bundles#getDirectory()}; it <b>MUST</b>
865 * be set.
866 *
867 * @return TRUE if the file was deleted
868 */
869 public boolean deleteFile() {
870 return deleteFile(Bundles.getDirectory());
871 }
872
873 /**
874 * Delete the .properties file.
875 * <p>
876 * Will use the most likely candidate as base if the file does not already
877 * exists and this resource is translatable (for instance, "en_US" will use
878 * "en" as a base if the resource is a translation file).
879 *
880 * @param path
881 * the path where the .properties files are, <b>MUST NOT</b> be
882 * NULL
883 *
884 * @return TRUE if the file was deleted
885 */
886 public boolean deleteFile(String path) {
887 File file = getUpdateFile(path);
888 return file.delete();
889 }
890
891 /**
892 * The description {@link TransBundle}, that is, a {@link TransBundle}
893 * dedicated to the description of the values of the given {@link Bundle}
894 * (can be NULL).
895 *
896 * @return the description {@link TransBundle}
897 */
898 public TransBundle<E> getDescriptionBundle() {
899 return descriptionBundle;
900 }
901
902 /**
903 * Reload the {@link Bundle} data files.
904 *
905 * @param resetToDefault
906 * reset to the default configuration (do not look into the
907 * possible user configuration files, only take the original
908 * configuration)
909 */
910 public void reload(boolean resetToDefault) {
911 setBundle(keyType, Locale.getDefault(), resetToDefault);
912 }
913
914 /**
915 * Check if the internal map contains the given key.
916 *
917 * @param key
918 * the key to check for
919 *
920 * @return true if it does
921 */
922 protected boolean containsKey(String key) {
923 return changeMap.containsKey(key) || map.containsKey(key);
924 }
925
926 /**
927 * Get the value for the given key if it exists in the internal map, or
928 * <tt>def</tt> if not.
929 *
930 * @param key
931 * the key to check for
932 * @param def
933 * the default value when it is not present in the internal map
934 *
935 * @return the value, or <tt>def</tt> if not found
936 */
937 protected String getString(String key, String def) {
938 if (changeMap.containsKey(key)) {
939 return changeMap.get(key);
940 }
941
942 if (map.containsKey(key)) {
943 return map.get(key);
944 }
945
946 return def;
947 }
948
949 /**
950 * Set the value for this key, in the change map (it is kept in memory, not
951 * yet on disk).
952 *
953 * @param key
954 * the key
955 * @param value
956 * the associated value
957 */
958 protected void setString(String key, String value) {
959 changeMap.put(key, value == null ? null : value.trim());
960 }
961
962 /**
963 * Return formated, display-able information from the {@link Meta} field
964 * given. Each line will always starts with a "#" character.
965 *
966 * @param meta
967 * the {@link Meta} field
968 *
969 * @return the information to display or NULL if none
970 */
971 protected String getMetaInfo(Meta meta) {
972 String desc = meta.description();
973 boolean group = meta.group();
974 Meta.Format format = meta.format();
975 String[] list = meta.list();
976 boolean nullable = meta.nullable();
977 String def = meta.def();
978 boolean array = meta.array();
979
980 // Default, empty values -> NULL
981 if (desc.length() + list.length + def.length() == 0 && !group
982 && nullable && format == Format.STRING) {
983 return null;
984 }
985
986 StringBuilder builder = new StringBuilder();
987 for (String line : desc.split("\n")) {
988 builder.append("# ").append(line).append("\n");
989 }
990
991 if (group) {
992 builder.append("# This item is used as a group, its content is not expected to be used.");
993 } else {
994 builder.append("# (FORMAT: ").append(format)
995 .append(nullable ? "" : ", required");
996 builder.append(") ");
997
998 if (list.length > 0) {
999 builder.append("\n# ALLOWED VALUES: ");
1000 boolean first = true;
1001 for (String value : list) {
1002 if (!first) {
1003 builder.append(", ");
1004 }
1005 builder.append(BundleHelper.escape(value));
1006 first = false;
1007 }
1008 }
1009
1010 if (array) {
1011 builder.append("\n# (This item accepts a list of ^escaped comma-separated values)");
1012 }
1013 }
1014
1015 return builder.toString();
1016 }
1017
1018 /**
1019 * The display name used in the <tt>.properties file</tt>.
1020 *
1021 * @return the name
1022 */
1023 protected String getBundleDisplayName() {
1024 return keyType.toString();
1025 }
1026
1027 /**
1028 * Write the header found in the configuration <tt>.properties</tt> file of
1029 * this {@link Bundles}.
1030 *
1031 * @param writer
1032 * the {@link Writer} to write the header in
1033 *
1034 * @throws IOException
1035 * in case of IO error
1036 */
1037 protected void writeHeader(Writer writer) throws IOException {
1038 writer.write("# " + getBundleDisplayName() + "\n");
1039 writer.write("#\n");
1040 }
1041
1042 /**
1043 * Write the given data to the config file, i.e., "MY_ID = my_curent_value"
1044 * followed by a new line.
1045 * <p>
1046 * Will prepend a # sign if the is is not set (see
1047 * {@link Bundle#isSet(Enum, boolean)}).
1048 *
1049 * @param writer
1050 * the {@link Writer} to write into
1051 * @param id
1052 * the id to write
1053 *
1054 * @throws IOException
1055 * in case of IO error
1056 */
1057 protected void writeValue(Writer writer, E id) throws IOException {
1058 boolean set = isSet(id, false);
1059 writeValue(writer, id.name(), getString(id), set);
1060 }
1061
1062 /**
1063 * Write the given data to the config file, i.e., "MY_ID = my_curent_value"
1064 * followed by a new line.
1065 * <p>
1066 * Will prepend a # sign if the is is not set.
1067 *
1068 * @param writer
1069 * the {@link Writer} to write into
1070 * @param id
1071 * the id to write
1072 * @param value
1073 * the id's value
1074 * @param set
1075 * the value is set in this {@link Bundle}
1076 *
1077 * @throws IOException
1078 * in case of IO error
1079 */
1080 protected void writeValue(Writer writer, String id, String value,
1081 boolean set) throws IOException {
1082
1083 if (!set) {
1084 writer.write('#');
1085 }
1086
1087 writer.write(id);
1088 writer.write(" = ");
1089
1090 if (value == null) {
1091 value = "";
1092 }
1093
1094 String[] lines = value.replaceAll("\t", "\\\\\\t").split("\n");
1095 for (int i = 0; i < lines.length; i++) {
1096 writer.write(lines[i]);
1097 if (i < lines.length - 1) {
1098 writer.write("\\n\\");
1099 }
1100 writer.write("\n");
1101 }
1102 }
1103
1104 /**
1105 * Return the source file for this {@link Bundles} from the given path.
1106 *
1107 * @param path
1108 * the path where the .properties files are
1109 *
1110 * @return the source {@link File}
1111 */
1112 protected File getUpdateFile(String path) {
1113 return new File(path, keyType.name() + ".properties");
1114 }
1115
1116 /**
1117 * Change the currently used bundle, and reset all changes.
1118 *
1119 * @param name
1120 * the name of the bundle to load
1121 * @param locale
1122 * the {@link Locale} to use
1123 * @param resetToDefault
1124 * reset to the default configuration (do not look into the
1125 * possible user configuration files, only take the original
1126 * configuration)
1127 */
1128 protected void setBundle(Enum<?> name, Locale locale, boolean resetToDefault) {
1129 changeMap.clear();
1130 String dir = Bundles.getDirectory();
1131 String bname = type.getPackage().getName() + "." + name.name();
1132
1133 boolean found = false;
1134 if (!resetToDefault && dir != null) {
1135 // Look into Bundles.getDirectory() for .properties files
1136 try {
1137 File file = getPropertyFile(dir, name.name(), locale);
1138 if (file != null) {
1139 Reader reader = new InputStreamReader(new FileInputStream(
1140 file), "UTF-8");
1141 resetMap(new PropertyResourceBundle(reader));
1142 found = true;
1143 }
1144 } catch (IOException e) {
1145 e.printStackTrace();
1146 }
1147 }
1148
1149 if (!found) {
1150 // Look into the package itself for resources
1151 try {
1152 resetMap(ResourceBundle
1153 .getBundle(bname, locale, type.getClassLoader(),
1154 new FixedResourceBundleControl()));
1155 found = true;
1156 } catch (MissingResourceException e) {
1157 } catch (Exception e) {
1158 e.printStackTrace();
1159 }
1160 }
1161
1162 if (!found) {
1163 // We have no bundle for this Bundle
1164 System.err.println("No bundle found for: " + bname);
1165 resetMap(null);
1166 }
1167 }
1168
1169 /**
1170 * Reset the backing map to the content of the given bundle, or with NULL
1171 * values if bundle is NULL.
1172 *
1173 * @param bundle
1174 * the bundle to copy
1175 */
1176 protected void resetMap(ResourceBundle bundle) {
1177 this.map.clear();
1178 for (Field field : type.getDeclaredFields()) {
1179 try {
1180 Meta meta = field.getAnnotation(Meta.class);
1181 if (meta != null) {
1182 E id = Enum.valueOf(type, field.getName());
1183
1184 String value;
1185 if (bundle != null) {
1186 value = bundle.getString(id.name());
1187 } else {
1188 value = null;
1189 }
1190
1191 this.map.put(id.name(), value == null ? null : value.trim());
1192 }
1193 } catch (MissingResourceException e) {
1194 }
1195 }
1196 }
1197
1198 /**
1199 * Take a snapshot of the changes in memory in this {@link Bundle} made by
1200 * the "set" methods ( {@link Bundle#setString(Enum, String)}...) at the
1201 * current time.
1202 *
1203 * @return a snapshot to use with {@link Bundle#restoreSnapshot(Object)}
1204 */
1205 public Object takeSnapshot() {
1206 return new HashMap<String, String>(changeMap);
1207 }
1208
1209 /**
1210 * Restore a snapshot taken with {@link Bundle}, or reset the current
1211 * changes if the snapshot is NULL.
1212 *
1213 * @param snap
1214 * the snapshot or NULL
1215 */
1216 @SuppressWarnings("unchecked")
1217 public void restoreSnapshot(Object snap) {
1218 if (snap == null) {
1219 changeMap.clear();
1220 } else {
1221 if (snap instanceof Map) {
1222 changeMap = (Map<String, String>) snap;
1223 } else {
1224 throw new RuntimeException(
1225 "Restoring changes in a Bundle must be done on a changes snapshot, "
1226 + "or NULL to discard current changes");
1227 }
1228 }
1229 }
1230
1231 /**
1232 * Return the resource file that is closer to the {@link Locale}.
1233 *
1234 * @param dir
1235 * the directory to look into
1236 * @param name
1237 * the file base name (without <tt>.properties</tt>)
1238 * @param locale
1239 * the {@link Locale}
1240 *
1241 * @return the closest match or NULL if none
1242 */
1243 private File getPropertyFile(String dir, String name, Locale locale) {
1244 List<String> locales = new ArrayList<String>();
1245 if (locale != null) {
1246 String country = locale.getCountry() == null ? "" : locale
1247 .getCountry();
1248 String language = locale.getLanguage() == null ? "" : locale
1249 .getLanguage();
1250 if (!language.isEmpty() && !country.isEmpty()) {
1251 locales.add("_" + language + "-" + country);
1252 }
1253 if (!language.isEmpty()) {
1254 locales.add("_" + language);
1255 }
1256 }
1257
1258 locales.add("");
1259
1260 File file = null;
1261 for (String loc : locales) {
1262 file = new File(dir, name + loc + ".properties");
1263 if (file.exists()) {
1264 break;
1265 }
1266
1267 file = null;
1268 }
1269
1270 return file;
1271 }
1272 }