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