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