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