add time in default trace handler
[nikiroo-utils.git] / src / be / nikiroo / utils / resources / Bundle.java
CommitLineData
ec1f3444
NR
1package be.nikiroo.utils.resources;
2
3import java.io.BufferedWriter;
4import java.io.File;
5import java.io.FileInputStream;
6import java.io.FileOutputStream;
7import java.io.IOException;
8import java.io.InputStreamReader;
9import java.io.OutputStreamWriter;
10import java.io.Reader;
ec1f3444
NR
11import java.io.Writer;
12import java.lang.reflect.Field;
13import java.util.ArrayList;
62c9ec78 14import java.util.HashMap;
ec1f3444
NR
15import java.util.List;
16import java.util.Locale;
62c9ec78 17import java.util.Map;
ec1f3444
NR
18import java.util.MissingResourceException;
19import java.util.PropertyResourceBundle;
20import java.util.ResourceBundle;
21
ec1f3444 22/**
62c9ec78 23 * This class encapsulate a {@link ResourceBundle} in UTF-8. It allows to
ec1f3444
NR
24 * retrieve values associated to an enumeration, and allows some additional
25 * methods.
62c9ec78
NR
26 * <p>
27 * It also sports a writable change map, and you can save back the
28 * {@link Bundle} to file with {@link Bundle#updateFile(String)}.
ec1f3444 29 *
ec1f3444
NR
30 * @param <E>
31 * the enum to use to get values out of this class
db31c358
NR
32 *
33 * @author niki
ec1f3444 34 */
db31c358 35
ec1f3444 36public class Bundle<E extends Enum<E>> {
db31c358 37 /** The type of E. */
ec1f3444 38 protected Class<E> type;
db31c358
NR
39 /**
40 * The {@link Enum} associated to this {@link Bundle} (all the keys used in
41 * this {@link Bundle} will be of this type).
42 */
43 protected Enum<?> keyType;
44
45 private TransBundle<E> descriptionBundle;
46
47 /** R/O map */
48 private Map<String, String> map;
49 /** R/W map */
50 private Map<String, String> changeMap;
ec1f3444
NR
51
52 /**
53 * Create a new {@link Bundles} of the given name.
54 *
55 * @param type
56 * a runtime instance of the class of E
ec1f3444
NR
57 * @param name
58 * the name of the {@link Bundles}
db31c358
NR
59 * @param descriptionBundle
60 * the description {@link TransBundle}, that is, a
61 * {@link TransBundle} dedicated to the description of the values
62 * of the given {@link Bundle} (can be NULL)
ec1f3444 63 */
db31c358
NR
64 protected Bundle(Class<E> type, Enum<?> name,
65 TransBundle<E> descriptionBundle) {
ec1f3444 66 this.type = type;
db31c358
NR
67 this.keyType = name;
68 this.descriptionBundle = descriptionBundle;
69
487926f7 70 this.map = new HashMap<String, String>();
62c9ec78 71 this.changeMap = new HashMap<String, String>();
e9ca6bb8 72 setBundle(name, Locale.getDefault(), false);
ec1f3444
NR
73 }
74
75 /**
76 * Return the value associated to the given id as a {@link String}.
77 *
db31c358 78 * @param id
ec1f3444
NR
79 * the id of the value to get
80 *
81 * @return the associated value, or NULL if not found (not present in the
82 * resource file)
83 */
84 public String getString(E id) {
487926f7 85 return getString(id.name());
ec1f3444
NR
86 }
87
62c9ec78
NR
88 /**
89 * Set the value associated to the given id as a {@link String}.
90 *
db31c358 91 * @param id
62c9ec78
NR
92 * the id of the value to get
93 * @param value
94 * the value
95 *
96 */
97 public void setString(E id, String value) {
487926f7 98 setString(id.name(), value);
62c9ec78
NR
99 }
100
ec1f3444
NR
101 /**
102 * Return the value associated to the given id as a {@link String} suffixed
103 * with the runtime value "_suffix" (that is, "_" and suffix).
487926f7
NR
104 * <p>
105 * Will only accept suffixes that form an existing id.
ec1f3444 106 *
db31c358 107 * @param id
ec1f3444
NR
108 * the id of the value to get
109 * @param suffix
110 * the runtime suffix
111 *
112 * @return the associated value, or NULL if not found (not present in the
113 * resource file)
114 */
115 public String getStringX(E id, String suffix) {
116 String key = id.name()
80383c14 117 + (suffix == null ? "" : "_" + suffix.toUpperCase());
ec1f3444 118
487926f7
NR
119 try {
120 id = Enum.valueOf(type, key);
121 return getString(id);
122 } catch (IllegalArgumentException e) {
123
ec1f3444
NR
124 }
125
126 return null;
127 }
128
62c9ec78
NR
129 /**
130 * Set the value associated to the given id as a {@link String} suffixed
131 * with the runtime value "_suffix" (that is, "_" and suffix).
487926f7
NR
132 * <p>
133 * Will only accept suffixes that form an existing id.
62c9ec78 134 *
db31c358 135 * @param id
62c9ec78
NR
136 * the id of the value to get
137 * @param suffix
138 * the runtime suffix
139 * @param value
140 * the value
141 */
142 public void setStringX(E id, String suffix, String value) {
143 String key = id.name()
144 + (suffix == null ? "" : "_" + suffix.toUpperCase());
145
487926f7
NR
146 try {
147 id = Enum.valueOf(type, key);
148 setString(id, value);
149 } catch (IllegalArgumentException e) {
150
151 }
62c9ec78
NR
152 }
153
ec1f3444
NR
154 /**
155 * Return the value associated to the given id as a {@link Boolean}.
156 *
db31c358 157 * @param id
ec1f3444
NR
158 * the id of the value to get
159 *
160 * @return the associated value
161 */
162 public Boolean getBoolean(E id) {
163 String str = getString(id);
164 if (str != null && str.length() > 0) {
165 if (str.equalsIgnoreCase("true") || str.equalsIgnoreCase("on")
166 || str.equalsIgnoreCase("yes"))
167 return true;
168 if (str.equalsIgnoreCase("false") || str.equalsIgnoreCase("off")
169 || str.equalsIgnoreCase("no"))
170 return false;
171
172 }
173
174 return null;
175 }
176
177 /**
62c9ec78 178 * Return the value associated to the given id as a {@link Boolean}.
ec1f3444 179 *
db31c358 180 * @param id
ec1f3444
NR
181 * the id of the value to get
182 * @param def
183 * the default value when it is not present in the config file or
184 * if it is not a boolean value
185 *
186 * @return the associated value
187 */
188 public boolean getBoolean(E id, boolean def) {
189 Boolean b = getBoolean(id);
190 if (b != null)
191 return b;
192
193 return def;
194 }
195
196 /**
197 * Return the value associated to the given id as an {@link Integer}.
198 *
db31c358 199 * @param id
ec1f3444
NR
200 * the id of the value to get
201 *
202 * @return the associated value
203 */
204 public Integer getInteger(E id) {
205 try {
206 return Integer.parseInt(getString(id));
207 } catch (Exception e) {
208 }
209
210 return null;
211 }
212
213 /**
db31c358 214 * Return the value associated to the given id as an int.
ec1f3444 215 *
db31c358 216 * @param id
ec1f3444
NR
217 * the id of the value to get
218 * @param def
219 * the default value when it is not present in the config file or
220 * if it is not a int value
221 *
222 * @return the associated value
223 */
224 public int getInteger(E id, int def) {
225 Integer i = getInteger(id);
226 if (i != null)
227 return i;
228
229 return def;
230 }
231
232 /**
233 * Return the value associated to the given id as a {@link Character}.
234 *
db31c358 235 * @param id
ec1f3444
NR
236 * the id of the value to get
237 *
238 * @return the associated value
239 */
62c9ec78 240 public Character getCharacter(E id) {
ec1f3444
NR
241 String s = getString(id).trim();
242 if (s.length() > 0) {
243 return s.charAt(0);
244 }
245
62c9ec78
NR
246 return null;
247 }
248
249 /**
250 * Return the value associated to the given id as a {@link Character}.
251 *
db31c358 252 * @param id
62c9ec78
NR
253 * the id of the value to get
254 * @param def
255 * the default value when it is not present in the config file or
256 * if it is not a char value
257 *
258 * @return the associated value
259 */
260 public char getCharacter(E id, char def) {
e8aa5bf9
NR
261 String s = getString(id);
262 if (s != null && s.length() > 0) {
263 return s.trim().charAt(0);
62c9ec78
NR
264 }
265
266 return def;
ec1f3444
NR
267 }
268
b3aad1f9 269 /**
80500544
NR
270 * Return the value associated to the given id as a colour if it is found
271 * and can be parsed.
272 * <p>
273 * The returned value is an ARGB value.
b3aad1f9 274 *
db31c358
NR
275 * @param id
276 * the id of the value to get
b3aad1f9
NR
277 *
278 * @return the associated value
279 */
80500544
NR
280 public Integer getColor(E id) {
281 Integer rep = null;
b3aad1f9
NR
282
283 String bg = getString(id).trim();
80500544
NR
284
285 int r = 0, g = 0, b = 0, a = -1;
62c9ec78 286 if (bg.startsWith("#") && (bg.length() == 7 || bg.length() == 9)) {
b3aad1f9 287 try {
80500544
NR
288 r = Integer.parseInt(bg.substring(1, 3), 16);
289 g = Integer.parseInt(bg.substring(3, 5), 16);
290 b = Integer.parseInt(bg.substring(5, 7), 16);
62c9ec78
NR
291 if (bg.length() == 9) {
292 a = Integer.parseInt(bg.substring(7, 9), 16);
80500544
NR
293 } else {
294 a = 255;
62c9ec78 295 }
80500544 296
b3aad1f9 297 } catch (NumberFormatException e) {
80500544 298 // no changes
b3aad1f9
NR
299 }
300 }
301
db31c358 302 // Try by name if still not found
80500544
NR
303 if (a == -1) {
304 if ("black".equalsIgnoreCase(bg)) {
305 a = 255;
306 r = 0;
307 g = 0;
308 b = 0;
309 } else if ("white".equalsIgnoreCase(bg)) {
310 a = 255;
311 r = 255;
312 g = 255;
313 b = 255;
314 } else if ("red".equalsIgnoreCase(bg)) {
315 a = 255;
316 r = 255;
317 g = 0;
318 b = 0;
319 } else if ("green".equalsIgnoreCase(bg)) {
320 a = 255;
321 r = 0;
322 g = 255;
323 b = 0;
324 } else if ("blue".equalsIgnoreCase(bg)) {
325 a = 255;
326 r = 0;
327 g = 0;
328 b = 255;
329 } else if ("grey".equalsIgnoreCase(bg)
330 || "gray".equalsIgnoreCase(bg)) {
331 a = 255;
332 r = 128;
333 g = 128;
334 b = 128;
335 } else if ("cyan".equalsIgnoreCase(bg)) {
336 a = 255;
337 r = 0;
338 g = 255;
339 b = 255;
340 } else if ("magenta".equalsIgnoreCase(bg)) {
341 a = 255;
342 r = 255;
343 g = 0;
344 b = 255;
345 } else if ("yellow".equalsIgnoreCase(bg)) {
346 a = 255;
347 r = 255;
348 g = 255;
349 b = 0;
db31c358
NR
350 }
351 }
db31c358 352
80500544
NR
353 if (a != -1) {
354 rep = ((a & 0xFF) << 24) //
355 | ((r & 0xFF) << 16) //
356 | ((g & 0xFF) << 8) //
357 | ((b & 0xFF) << 0);
358 }
359
360 return rep;
b3aad1f9
NR
361 }
362
62c9ec78 363 /**
80500544
NR
364 * Set the value associated to the given id as a colour.
365 * <p>
366 * The value is an BGRA value.
62c9ec78 367 *
db31c358
NR
368 * @param id
369 * the id of the value to set
370 * @param color
80500544 371 * the new colour
62c9ec78 372 */
80500544
NR
373 public void setColor(E id, Integer color) {
374 int a = (color >> 24) & 0xFF;
375 int r = (color >> 16) & 0xFF;
376 int g = (color >> 8) & 0xFF;
377 int b = (color >> 0) & 0xFF;
378
379 String rs = Integer.toString(r, 16);
380 String gs = Integer.toString(g, 16);
381 String bs = Integer.toString(b, 16);
382 String as = "";
383 if (a < 255) {
384 as = Integer.toString(a, 16);
62c9ec78
NR
385 }
386
80500544 387 setString(id, "#" + rs + gs + bs + as);
62c9ec78
NR
388 }
389
7c192d3c
NR
390 /**
391 * Return the value associated to the given id as a list of values if it is
392 * found and can be parsed.
393 *
394 * @param id
395 * the id of the value to get
396 *
397 * @return the associated list, empty if the value is empty, NULL if it is
398 * not found or cannot be parsed as a list
399 */
400 public List<String> getList(E id) {
401 List<String> list = new ArrayList<String>();
402 String raw = getString(id);
403 try {
404 boolean inQuote = false;
405 boolean prevIsBackSlash = false;
406 StringBuilder builder = new StringBuilder();
407 for (int i = 0; i < raw.length(); i++) {
408 char car = raw.charAt(i);
409
410 if (prevIsBackSlash) {
411 builder.append(car);
412 prevIsBackSlash = false;
413 } else {
414 switch (car) {
415 case '"':
416 if (inQuote) {
417 list.add(builder.toString());
418 builder.setLength(0);
419 }
420
421 inQuote = !inQuote;
422 break;
423 case '\\':
424 prevIsBackSlash = true;
425 break;
426 case ' ':
427 case '\n':
428 case '\r':
429 if (inQuote) {
430 builder.append(car);
431 }
432 break;
433
434 case ',':
435 if (!inQuote) {
436 break;
437 }
438 // continue to default
439 default:
440 if (!inQuote) {
441 // Bad format!
442 return null;
443 }
444
445 builder.append(car);
446 break;
447 }
448 }
449 }
450
451 if (inQuote || prevIsBackSlash) {
452 // Bad format!
453 return null;
454 }
455
456 } catch (Exception e) {
457 return null;
458 }
459
460 return list;
461 }
462
463 /**
464 * Set the value associated to the given id as a list of values.
465 *
466 * @param id
467 * the id of the value to set
468 * @param list
469 * the new list of values
470 */
471 public void setList(E id, List<String> list) {
472 StringBuilder builder = new StringBuilder();
473 for (String item : list) {
474 if (builder.length() > 0) {
475 builder.append(", ");
476 }
477 builder.append('"')//
478 .append(item.replace("\\", "\\\\").replace("\"", "\\\""))//
479 .append('"');
480 }
481
482 setString(id, builder.toString());
483 }
484
487926f7
NR
485 /**
486 * Create/update the .properties file.
487 * <p>
488 * Will use the most likely candidate as base if the file does not already
489 * exists and this resource is translatable (for instance, "en_US" will use
490 * "en" as a base if the resource is a translation file).
491 * <p>
492 * Will update the files in {@link Bundles#getDirectory()}; it <b>MUST</b>
493 * be set.
494 *
495 * @throws IOException
496 * in case of IO errors
497 */
498 public void updateFile() throws IOException {
499 updateFile(Bundles.getDirectory());
500 }
501
ec1f3444 502 /**
80383c14
NR
503 * Create/update the .properties file.
504 * <p>
505 * Will use the most likely candidate as base if the file does not already
506 * exists and this resource is translatable (for instance, "en_US" will use
507 * "en" as a base if the resource is a translation file).
ec1f3444
NR
508 *
509 * @param path
487926f7
NR
510 * the path where the .properties files are, <b>MUST NOT</b> be
511 * NULL
ec1f3444
NR
512 *
513 * @throws IOException
514 * in case of IO errors
515 */
516 public void updateFile(String path) throws IOException {
517 File file = getUpdateFile(path);
518
519 BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(
520 new FileOutputStream(file), "UTF-8"));
521
522 writeHeader(writer);
523 writer.write("\n");
524 writer.write("\n");
525
526 for (Field field : type.getDeclaredFields()) {
527 Meta meta = field.getAnnotation(Meta.class);
528 if (meta != null) {
cd0c27d2 529 E id = Enum.valueOf(type, field.getName());
ec1f3444
NR
530 String info = getMetaInfo(meta);
531
532 if (info != null) {
533 writer.write(info);
534 writer.write("\n");
535 }
536
537 writeValue(writer, id);
538 }
539 }
540
541 writer.close();
542 }
543
e8aa5bf9
NR
544 /**
545 * Delete the .properties file.
546 * <p>
547 * Will use the most likely candidate as base if the file does not already
548 * exists and this resource is translatable (for instance, "en_US" will use
549 * "en" as a base if the resource is a translation file).
550 * <p>
551 * Will delete the files in {@link Bundles#getDirectory()}; it <b>MUST</b>
552 * be set.
553 *
554 * @return TRUE if the file was deleted
555 */
556 public boolean deleteFile() {
557 return deleteFile(Bundles.getDirectory());
558 }
559
560 /**
561 * Delete the .properties file.
562 * <p>
563 * Will use the most likely candidate as base if the file does not already
564 * exists and this resource is translatable (for instance, "en_US" will use
565 * "en" as a base if the resource is a translation file).
566 *
567 * @param path
568 * the path where the .properties files are, <b>MUST NOT</b> be
569 * NULL
570 *
571 * @return TRUE if the file was deleted
572 */
573 public boolean deleteFile(String path) {
574 File file = getUpdateFile(path);
575 return file.delete();
576 }
577
db31c358
NR
578 /**
579 * The description {@link TransBundle}, that is, a {@link TransBundle}
580 * dedicated to the description of the values of the given {@link Bundle}
581 * (can be NULL).
582 *
583 * @return the description {@link TransBundle}
584 */
585 public TransBundle<E> getDescriptionBundle() {
586 return descriptionBundle;
587 }
588
80383c14
NR
589 /**
590 * Reload the {@link Bundle} data files.
e9ca6bb8
NR
591 *
592 * @param resetToDefault
593 * reset to the default configuration (do not look into the
594 * possible user configuration files, only take the original
595 * configuration)
80383c14 596 */
e9ca6bb8 597 public void reload(boolean resetToDefault) {
db31c358 598 setBundle(keyType, Locale.getDefault(), resetToDefault);
80383c14
NR
599 }
600
ec1f3444
NR
601 /**
602 * Check if the internal map contains the given key.
603 *
604 * @param key
605 * the key to check for
606 *
607 * @return true if it does
608 */
609 protected boolean containsKey(String key) {
487926f7 610 return changeMap.containsKey(key) || map.containsKey(key);
ec1f3444
NR
611 }
612
613 /**
2cce3dcb
NR
614 * Get the value for the given key if it exists in the internal map, or NULL
615 * if not.
ec1f3444
NR
616 *
617 * @param key
618 * the key to check for
619 *
2cce3dcb 620 * @return the value, or NULL
ec1f3444
NR
621 */
622 protected String getString(String key) {
62c9ec78
NR
623 if (changeMap.containsKey(key)) {
624 return changeMap.get(key);
625 }
626
487926f7
NR
627 if (map.containsKey(key)) {
628 return map.get(key);
ec1f3444
NR
629 }
630
631 return null;
632 }
633
62c9ec78
NR
634 /**
635 * Set the value for this key, in the change map (it is kept in memory, not
636 * yet on disk).
637 *
638 * @param key
639 * the key
640 * @param value
641 * the associated value
642 */
643 protected void setString(String key, String value) {
487926f7 644 changeMap.put(key, value == null ? null : value.trim());
62c9ec78
NR
645 }
646
ec1f3444
NR
647 /**
648 * Return formated, display-able information from the {@link Meta} field
649 * given. Each line will always starts with a "#" character.
650 *
651 * @param meta
652 * the {@link Meta} field
653 *
654 * @return the information to display or NULL if none
655 */
656 protected String getMetaInfo(Meta meta) {
db31c358
NR
657 String desc = meta.description();
658 boolean group = meta.group();
659 Meta.Format format = meta.format();
660 String[] list = meta.list();
661 boolean nullable = meta.nullable();
e8aa5bf9 662 String def = meta.def();
ec1f3444 663 String info = meta.info();
db31c358 664 boolean array = meta.array();
ec1f3444 665
db31c358 666 // Default, empty values -> NULL
e8aa5bf9
NR
667 if (desc.length() + list.length + info.length() + def.length() == 0
668 && !group && nullable && format == Meta.Format.STRING) {
ec1f3444 669 return null;
db31c358 670 }
ec1f3444
NR
671
672 StringBuilder builder = new StringBuilder();
db31c358
NR
673 builder.append("# ").append(desc);
674 if (desc.length() > 20) {
675 builder.append("\n#");
676 }
ec1f3444 677
db31c358
NR
678 if (group) {
679 builder.append("This item is used as a group, its content is not expected to be used.");
680 } else {
681 builder.append(" (FORMAT: ").append(format)
682 .append(nullable ? "" : " (required)");
683 builder.append(") ").append(info);
684
685 if (list.length > 0) {
686 builder.append("\n# ALLOWED VALUES:");
687 for (String value : list) {
688 builder.append(" \"").append(value).append("\"");
689 }
ec1f3444
NR
690 }
691
db31c358
NR
692 if (array) {
693 builder.append("\n# (This item accept a list of comma-separated values)");
ec1f3444
NR
694 }
695 }
696
ec1f3444
NR
697 return builder.toString();
698 }
699
700 /**
701 * The display name used in the <tt>.properties file</tt>.
702 *
703 * @return the name
704 */
705 protected String getBundleDisplayName() {
db31c358 706 return keyType.toString();
ec1f3444
NR
707 }
708
709 /**
710 * Write the header found in the configuration <tt>.properties</tt> file of
711 * this {@link Bundles}.
712 *
713 * @param writer
714 * the {@link Writer} to write the header in
715 *
716 * @throws IOException
717 * in case of IO error
718 */
719 protected void writeHeader(Writer writer) throws IOException {
720 writer.write("# " + getBundleDisplayName() + "\n");
721 writer.write("#\n");
722 }
723
724 /**
725 * Write the given id to the config file, i.e., "MY_ID = my_curent_value"
726 * followed by a new line
727 *
728 * @param writer
729 * the {@link Writer} to write into
730 * @param id
731 * the id to write
732 *
733 * @throws IOException
734 * in case of IO error
735 */
736 protected void writeValue(Writer writer, E id) throws IOException {
737 writeValue(writer, id.name(), getString(id));
738 }
739
740 /**
741 * Write the given data to the config file, i.e., "MY_ID = my_curent_value"
742 * followed by a new line
743 *
744 * @param writer
745 * the {@link Writer} to write into
746 * @param id
747 * the id to write
748 * @param value
749 * the id's value
750 *
751 * @throws IOException
752 * in case of IO error
753 */
754 protected void writeValue(Writer writer, String id, String value)
755 throws IOException {
756 writer.write(id);
757 writer.write(" = ");
758
759 if (value == null) {
760 value = "";
761 }
762
009196a4 763 String[] lines = value.replaceAll("\t", "\\\\\\t").split("\n");
ec1f3444
NR
764 for (int i = 0; i < lines.length; i++) {
765 writer.write(lines[i]);
766 if (i < lines.length - 1) {
767 writer.write("\\n\\");
768 }
769 writer.write("\n");
770 }
771 }
772
773 /**
774 * Return the source file for this {@link Bundles} from the given path.
775 *
776 * @param path
777 * the path where the .properties files are
778 *
779 * @return the source {@link File}
ec1f3444
NR
780 */
781 protected File getUpdateFile(String path) {
db31c358 782 return new File(path, keyType.name() + ".properties");
ec1f3444
NR
783 }
784
785 /**
62c9ec78 786 * Change the currently used bundle, and reset all changes.
ec1f3444
NR
787 *
788 * @param name
789 * the name of the bundle to load
790 * @param locale
791 * the {@link Locale} to use
e9ca6bb8
NR
792 * @param resetToDefault
793 * reset to the default configuration (do not look into the
794 * possible user configuration files, only take the original
795 * configuration)
ec1f3444 796 */
e9ca6bb8 797 protected void setBundle(Enum<?> name, Locale locale, boolean resetToDefault) {
62c9ec78 798 changeMap.clear();
ec1f3444 799 String dir = Bundles.getDirectory();
e8aa5bf9 800 String bname = type.getPackage().getName() + "." + name.name();
ec1f3444 801
487926f7 802 boolean found = false;
e9ca6bb8 803 if (!resetToDefault && dir != null) {
e8aa5bf9 804 // Look into Bundles.getDirectory() for .properties files
ec1f3444
NR
805 try {
806 File file = getPropertyFile(dir, name.name(), locale);
807 if (file != null) {
808 Reader reader = new InputStreamReader(new FileInputStream(
809 file), "UTF8");
487926f7
NR
810 resetMap(new PropertyResourceBundle(reader));
811 found = true;
ec1f3444
NR
812 }
813 } catch (IOException e) {
814 e.printStackTrace();
815 }
816 }
817
487926f7 818 if (!found) {
e8aa5bf9 819 // Look into the package itself for resources
e9ca6bb8 820 try {
487926f7
NR
821 resetMap(ResourceBundle
822 .getBundle(bname, locale, type.getClassLoader(),
823 new FixedResourceBundleControl()));
e8aa5bf9
NR
824 found = true;
825 } catch (MissingResourceException e) {
e9ca6bb8 826 } catch (Exception e) {
e8aa5bf9 827 e.printStackTrace();
487926f7
NR
828 }
829 }
e8aa5bf9
NR
830
831 if (!found) {
832 // We have no bundle for this Bundle
833 System.err.println("No bundle found for: " + bname);
834 resetMap(null);
835 }
487926f7
NR
836 }
837
838 /**
e8aa5bf9
NR
839 * Reset the backing map to the content of the given bundle, or with default
840 * valiues if bundle is NULL.
487926f7
NR
841 *
842 * @param bundle
843 * the bundle to copy
844 */
b771aed5 845 protected void resetMap(ResourceBundle bundle) {
487926f7 846 this.map.clear();
e8aa5bf9
NR
847 for (Field field : type.getDeclaredFields()) {
848 try {
849 Meta meta = field.getAnnotation(Meta.class);
850 if (meta != null) {
851 E id = Enum.valueOf(type, field.getName());
852
853 String value;
854 if (bundle != null) {
855 value = bundle.getString(id.name());
856 } else {
857 value = meta.def();
858 }
859
860 this.map.put(id.name(), value == null ? null : value.trim());
487926f7 861 }
e8aa5bf9 862 } catch (MissingResourceException e) {
e9ca6bb8
NR
863 }
864 }
865 }
cd0c27d2 866
e9ca6bb8
NR
867 /**
868 * Take a snapshot of the changes in memory in this {@link Bundle} made by
869 * the "set" methods ( {@link Bundle#setString(Enum, String)}...) at the
870 * current time.
871 *
487926f7 872 * @return a snapshot to use with {@link Bundle#restoreSnapshot(Object)}
e9ca6bb8 873 */
487926f7 874 public Object takeSnapshot() {
e9ca6bb8
NR
875 return new HashMap<String, String>(changeMap);
876 }
877
878 /**
879 * Restore a snapshot taken with {@link Bundle}, or reset the current
880 * changes if the snapshot is NULL.
881 *
882 * @param snap
883 * the snapshot or NULL
884 */
885 @SuppressWarnings("unchecked")
487926f7 886 public void restoreSnapshot(Object snap) {
e9ca6bb8
NR
887 if (snap == null) {
888 changeMap.clear();
889 } else {
890 if (snap instanceof Map) {
891 changeMap = (Map<String, String>) snap;
892 } else {
d827da2a 893 throw new RuntimeException(
e9ca6bb8
NR
894 "Restoring changes in a Bundle must be done on a changes snapshot, "
895 + "or NULL to discard current changes");
896 }
ec1f3444
NR
897 }
898 }
899
900 /**
901 * Return the resource file that is closer to the {@link Locale}.
902 *
903 * @param dir
db31c358 904 * the directory to look into
ec1f3444 905 * @param name
db31c358 906 * the file base name (without <tt>.properties</tt>)
ec1f3444
NR
907 * @param locale
908 * the {@link Locale}
909 *
910 * @return the closest match or NULL if none
911 */
912 private File getPropertyFile(String dir, String name, Locale locale) {
913 List<String> locales = new ArrayList<String>();
914 if (locale != null) {
915 String country = locale.getCountry() == null ? "" : locale
916 .getCountry();
917 String language = locale.getLanguage() == null ? "" : locale
918 .getLanguage();
919 if (!language.isEmpty() && !country.isEmpty()) {
920 locales.add("_" + language + "-" + country);
921 }
922 if (!language.isEmpty()) {
923 locales.add("_" + language);
924 }
925 }
926
927 locales.add("");
928
929 File file = null;
930 for (String loc : locales) {
931 file = new File(dir, name + loc + ".properties");
932 if (file.exists()) {
933 break;
ec1f3444 934 }
d827da2a 935
cd0c27d2 936 file = null;
ec1f3444
NR
937 }
938
939 return file;
940 }
941}