b4019de53d67e851e096074021b8b7341b22eef8
[nikiroo-utils.git] / src / be / nikiroo / utils / resources / Bundle.java
1 package be.nikiroo.utils.resources;
2
3 import java.awt.Color;
4 import java.io.BufferedWriter;
5 import java.io.File;
6 import java.io.FileInputStream;
7 import java.io.FileOutputStream;
8 import java.io.IOException;
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 /**
24 * This class encapsulate a {@link ResourceBundle} in UTF-8. It allows to
25 * retrieve values associated to an enumeration, and allows some additional
26 * methods.
27 * <p>
28 * It also sports a writable change map, and you can save back the
29 * {@link Bundle} to file with {@link Bundle#updateFile(String)}.
30 *
31 * @author niki
32 *
33 * @param <E>
34 * the enum to use to get values out of this class
35 */
36 public class Bundle<E extends Enum<E>> {
37 protected Class<E> type;
38 protected Enum<?> name;
39 private ResourceBundle map;
40 private Map<String, String> changeMap;
41
42 /**
43 * Create a new {@link Bundles} of the given name.
44 *
45 * @param type
46 * a runtime instance of the class of E
47 *
48 * @param name
49 * the name of the {@link Bundles}
50 */
51 protected Bundle(Class<E> type, Enum<?> name) {
52 this.type = type;
53 this.name = name;
54 this.changeMap = new HashMap<String, String>();
55 setBundle(name, Locale.getDefault());
56 }
57
58 /**
59 * Return the value associated to the given id as a {@link String}.
60 *
61 * @param mame
62 * the id of the value to get
63 *
64 * @return the associated value, or NULL if not found (not present in the
65 * resource file)
66 */
67 public String getString(E id) {
68 return getStringX(id, null);
69 }
70
71 /**
72 * Set the value associated to the given id as a {@link String}.
73 *
74 * @param mame
75 * the id of the value to get
76 * @param value
77 * the value
78 *
79 */
80 public void setString(E id, String value) {
81 setStringX(id, null, value);
82 }
83
84 /**
85 * Return the value associated to the given id as a {@link String} suffixed
86 * with the runtime value "_suffix" (that is, "_" and suffix).
87 *
88 * @param mame
89 * the id of the value to get
90 * @param suffix
91 * the runtime suffix
92 *
93 * @return the associated value, or NULL if not found (not present in the
94 * resource file)
95 */
96 public String getStringX(E id, String suffix) {
97 String key = id.name()
98 + (suffix == null ? "" : "_" + suffix.toUpperCase());
99
100 if (containsKey(key)) {
101 return getString(key).trim();
102 }
103
104 return null;
105 }
106
107 /**
108 * Set the value associated to the given id as a {@link String} suffixed
109 * with the runtime value "_suffix" (that is, "_" and suffix).
110 *
111 * @param mame
112 * the id of the value to get
113 * @param suffix
114 * the runtime suffix
115 * @param value
116 * the value
117 */
118 public void setStringX(E id, String suffix, String value) {
119 String key = id.name()
120 + (suffix == null ? "" : "_" + suffix.toUpperCase());
121
122 setString(key, value);
123 }
124
125 /**
126 * Return the value associated to the given id as a {@link Boolean}.
127 *
128 * @param mame
129 * the id of the value to get
130 *
131 * @return the associated value
132 */
133 public Boolean getBoolean(E id) {
134 String str = getString(id);
135 if (str != null && str.length() > 0) {
136 if (str.equalsIgnoreCase("true") || str.equalsIgnoreCase("on")
137 || str.equalsIgnoreCase("yes"))
138 return true;
139 if (str.equalsIgnoreCase("false") || str.equalsIgnoreCase("off")
140 || str.equalsIgnoreCase("no"))
141 return false;
142
143 }
144
145 return null;
146 }
147
148 /**
149 * Return the value associated to the given id as a {@link Boolean}.
150 *
151 * @param mame
152 * the id of the value to get
153 * @param def
154 * the default value when it is not present in the config file or
155 * if it is not a boolean value
156 *
157 * @return the associated value
158 */
159 public boolean getBoolean(E id, boolean def) {
160 Boolean b = getBoolean(id);
161 if (b != null)
162 return b;
163
164 return def;
165 }
166
167 /**
168 * Return the value associated to the given id as an {@link Integer}.
169 *
170 * @param mame
171 * the id of the value to get
172 *
173 * @return the associated value
174 */
175 public Integer getInteger(E id) {
176 try {
177 return Integer.parseInt(getString(id));
178 } catch (Exception e) {
179 }
180
181 return null;
182 }
183
184 /**
185 * Return the value associated to the given id as a {@link int}.
186 *
187 * @param mame
188 * the id of the value to get
189 * @param def
190 * the default value when it is not present in the config file or
191 * if it is not a int value
192 *
193 * @return the associated value
194 */
195 public int getInteger(E id, int def) {
196 Integer i = getInteger(id);
197 if (i != null)
198 return i;
199
200 return def;
201 }
202
203 /**
204 * Return the value associated to the given id as a {@link Character}.
205 *
206 * @param mame
207 * the id of the value to get
208 *
209 * @return the associated value
210 */
211 public Character getCharacter(E id) {
212 String s = getString(id).trim();
213 if (s.length() > 0) {
214 return s.charAt(0);
215 }
216
217 return null;
218 }
219
220 /**
221 * Return the value associated to the given id as a {@link Character}.
222 *
223 * @param mame
224 * the id of the value to get
225 * @param def
226 * the default value when it is not present in the config file or
227 * if it is not a char value
228 *
229 * @return the associated value
230 */
231 public char getCharacter(E id, char def) {
232 String s = getString(id).trim();
233 if (s.length() > 0) {
234 return s.charAt(0);
235 }
236
237 return def;
238 }
239
240 /**
241 * Return the value associated to the given id as a {@link Color}.
242 *
243 * @param the
244 * id of the value to get
245 *
246 * @return the associated value
247 */
248 public Color getColor(E id) {
249 Color color = null;
250
251 String bg = getString(id).trim();
252 if (bg.startsWith("#") && (bg.length() == 7 || bg.length() == 9)) {
253 try {
254 int r = Integer.parseInt(bg.substring(1, 3), 16);
255 int g = Integer.parseInt(bg.substring(3, 5), 16);
256 int b = Integer.parseInt(bg.substring(5, 7), 16);
257 int a = 255;
258 if (bg.length() == 9) {
259 a = Integer.parseInt(bg.substring(7, 9), 16);
260 }
261 color = new Color(r, g, b, a);
262 } catch (NumberFormatException e) {
263 color = null; // no changes
264 }
265 }
266
267 return color;
268 }
269
270 /**
271 * Set the value associated to the given id as a {@link Color}.
272 *
273 * @param the
274 * id of the value to get
275 *
276 * @return the associated value
277 */
278 public void setColor(E id, Color color) {
279 String r = Integer.toString(color.getRed(), 16);
280 String g = Integer.toString(color.getGreen(), 16);
281 String b = Integer.toString(color.getBlue(), 16);
282 String a = "";
283 if (color.getAlpha() < 255) {
284 a = Integer.toString(color.getAlpha(), 16);
285 }
286
287 setString(id, "#" + r + g + b + a);
288 }
289
290 /**
291 * Create/update the .properties file.
292 * <p>
293 * Will use the most likely candidate as base if the file does not already
294 * exists and this resource is translatable (for instance, "en_US" will use
295 * "en" as a base if the resource is a translation file).
296 *
297 * @param path
298 * the path where the .properties files are
299 *
300 * @throws IOException
301 * in case of IO errors
302 */
303 public void updateFile(String path) throws IOException {
304 File file = getUpdateFile(path);
305
306 BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(
307 new FileOutputStream(file), "UTF-8"));
308
309 writeHeader(writer);
310 writer.write("\n");
311 writer.write("\n");
312
313 for (Field field : type.getDeclaredFields()) {
314 Meta meta = field.getAnnotation(Meta.class);
315 if (meta != null) {
316 E id = E.valueOf(type, field.getName());
317 String info = getMetaInfo(meta);
318
319 if (info != null) {
320 writer.write(info);
321 writer.write("\n");
322 }
323
324 writeValue(writer, id);
325 }
326 }
327
328 writer.close();
329 }
330
331 /**
332 * Reload the {@link Bundle} data files.
333 */
334 public void reload() {
335 setBundle(name, null);
336 }
337
338 /**
339 * Check if the internal map contains the given key.
340 *
341 * @param key
342 * the key to check for
343 *
344 * @return true if it does
345 */
346 protected boolean containsKey(String key) {
347 if (changeMap.containsKey(key)) {
348 return true;
349 }
350
351 try {
352 map.getObject(key);
353 return true;
354 } catch (MissingResourceException e) {
355 return false;
356 }
357 }
358
359 /**
360 * Get the value for the given key if it exists in the internal map, or NULL
361 * if not.
362 *
363 * @param key
364 * the key to check for
365 *
366 * @return the value, or NULL
367 */
368 protected String getString(String key) {
369 if (changeMap.containsKey(key)) {
370 return changeMap.get(key);
371 }
372
373 if (containsKey(key)) {
374 return map.getString(key);
375 }
376
377 return null;
378 }
379
380 /**
381 * Set the value for this key, in the change map (it is kept in memory, not
382 * yet on disk).
383 *
384 * @param key
385 * the key
386 * @param value
387 * the associated value
388 */
389 protected void setString(String key, String value) {
390 changeMap.put(key, value);
391 }
392
393 /**
394 * Return formated, display-able information from the {@link Meta} field
395 * given. Each line will always starts with a "#" character.
396 *
397 * @param meta
398 * the {@link Meta} field
399 *
400 * @return the information to display or NULL if none
401 */
402 protected String getMetaInfo(Meta meta) {
403 String what = meta.what();
404 String where = meta.where();
405 String format = meta.format();
406 String info = meta.info();
407
408 int opt = what.length() + where.length() + format.length();
409 if (opt + info.length() == 0)
410 return null;
411
412 StringBuilder builder = new StringBuilder();
413 builder.append("# ");
414
415 if (opt > 0) {
416 builder.append("(");
417 if (what.length() > 0) {
418 builder.append("WHAT: " + what);
419 if (where.length() + format.length() > 0)
420 builder.append(", ");
421 }
422
423 if (where.length() > 0) {
424 builder.append("WHERE: " + where);
425 if (format.length() > 0)
426 builder.append(", ");
427 }
428
429 if (format.length() > 0) {
430 builder.append("FORMAT: " + format);
431 }
432
433 builder.append(")");
434 if (info.length() > 0) {
435 builder.append("\n# ");
436 }
437 }
438
439 builder.append(info);
440
441 return builder.toString();
442 }
443
444 /**
445 * The display name used in the <tt>.properties file</tt>.
446 *
447 * @return the name
448 */
449 protected String getBundleDisplayName() {
450 return name.toString();
451 }
452
453 /**
454 * Write the header found in the configuration <tt>.properties</tt> file of
455 * this {@link Bundles}.
456 *
457 * @param writer
458 * the {@link Writer} to write the header in
459 *
460 * @throws IOException
461 * in case of IO error
462 */
463 protected void writeHeader(Writer writer) throws IOException {
464 writer.write("# " + getBundleDisplayName() + "\n");
465 writer.write("#\n");
466 }
467
468 /**
469 * Write the given id to the config file, i.e., "MY_ID = my_curent_value"
470 * followed by a new line
471 *
472 * @param writer
473 * the {@link Writer} to write into
474 * @param id
475 * the id to write
476 *
477 * @throws IOException
478 * in case of IO error
479 */
480 protected void writeValue(Writer writer, E id) throws IOException {
481 writeValue(writer, id.name(), getString(id));
482 }
483
484 /**
485 * Write the given data to the config file, i.e., "MY_ID = my_curent_value"
486 * followed by a new line
487 *
488 * @param writer
489 * the {@link Writer} to write into
490 * @param id
491 * the id to write
492 * @param value
493 * the id's value
494 *
495 * @throws IOException
496 * in case of IO error
497 */
498 protected void writeValue(Writer writer, String id, String value)
499 throws IOException {
500 writer.write(id);
501 writer.write(" = ");
502
503 if (value == null) {
504 value = "";
505 }
506
507 String[] lines = value.replaceAll("\t", "\\\\\\t").split("\n");
508 for (int i = 0; i < lines.length; i++) {
509 writer.write(lines[i]);
510 if (i < lines.length - 1) {
511 writer.write("\\n\\");
512 }
513 writer.write("\n");
514 }
515 }
516
517 /**
518 * Return the source file for this {@link Bundles} from the given path.
519 *
520 * @param path
521 * the path where the .properties files are
522 *
523 * @return the source {@link File}
524 *
525 * @throws IOException
526 * in case of IO errors
527 */
528 protected File getUpdateFile(String path) {
529 return new File(path, name.name() + ".properties");
530 }
531
532 /**
533 * Change the currently used bundle, and reset all changes.
534 *
535 * @param name
536 * the name of the bundle to load
537 * @param locale
538 * the {@link Locale} to use
539 */
540 protected void setBundle(Enum<?> name, Locale locale) {
541 map = null;
542 changeMap.clear();
543 String dir = Bundles.getDirectory();
544
545 if (dir != null) {
546 try {
547 File file = getPropertyFile(dir, name.name(), locale);
548 if (file != null) {
549 Reader reader = new InputStreamReader(new FileInputStream(
550 file), "UTF8");
551 map = new PropertyResourceBundle(reader);
552 }
553 } catch (IOException e) {
554 e.printStackTrace();
555 }
556 }
557
558 if (map == null) {
559 map = ResourceBundle.getBundle(type.getPackage().getName() + "."
560 + name.name(), locale, new FixedResourceBundleControl());
561 }
562 }
563
564 /**
565 * Return the resource file that is closer to the {@link Locale}.
566 *
567 * @param dir
568 * the dirctory to look into
569 * @param name
570 * the file basename (without <tt>.properties</tt>)
571 * @param locale
572 * the {@link Locale}
573 *
574 * @return the closest match or NULL if none
575 */
576 private File getPropertyFile(String dir, String name, Locale locale) {
577 List<String> locales = new ArrayList<String>();
578 if (locale != null) {
579 String country = locale.getCountry() == null ? "" : locale
580 .getCountry();
581 String language = locale.getLanguage() == null ? "" : locale
582 .getLanguage();
583 if (!language.isEmpty() && !country.isEmpty()) {
584 locales.add("_" + language + "-" + country);
585 }
586 if (!language.isEmpty()) {
587 locales.add("_" + language);
588 }
589 }
590
591 locales.add("");
592
593 File file = null;
594 for (String loc : locales) {
595 file = new File(dir, name + loc + ".properties");
596 if (file.exists()) {
597 break;
598 } else {
599 file = null;
600 }
601 }
602
603 return file;
604 }
605 }