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