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