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