package be.nikiroo.utils.resources; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.Reader; import java.io.Writer; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.MissingResourceException; import java.util.PropertyResourceBundle; import java.util.ResourceBundle; import be.nikiroo.utils.resources.Meta.Format; /** * This class encapsulate a {@link ResourceBundle} in UTF-8. It allows to * retrieve values associated to an enumeration, and allows some additional * methods. *

* It also sports a writable change map, and you can save back the * {@link Bundle} to file with {@link Bundle#updateFile(String)}. * * @param * the enum to use to get values out of this class * * @author niki */ public class Bundle> { /** The type of E. */ protected Class type; /** * The {@link Enum} associated to this {@link Bundle} (all the keys used in * this {@link Bundle} will be of this type). */ protected Enum keyType; private TransBundle descriptionBundle; /** R/O map */ private Map map; /** R/W map */ private Map changeMap; /** * Create a new {@link Bundles} of the given name. * * @param type * a runtime instance of the class of E * @param name * the name of the {@link Bundles} * @param descriptionBundle * the description {@link TransBundle}, that is, a * {@link TransBundle} dedicated to the description of the values * of the given {@link Bundle} (can be NULL) */ protected Bundle(Class type, Enum name, TransBundle descriptionBundle) { this.type = type; this.keyType = name; this.descriptionBundle = descriptionBundle; this.map = new HashMap(); this.changeMap = new HashMap(); setBundle(name, Locale.getDefault(), false); } /** * Check if the setting is set into this {@link Bundle}. * * @param id * the id of the setting to check * @param includeDefaultValue * TRUE to only return false when the setting is not set AND * there is no default value * * @return TRUE if the setting is set */ public boolean isSet(E id, boolean includeDefaultValue) { return isSet(id.name(), includeDefaultValue); } /** * Check if the setting is set into this {@link Bundle}. * * @param name * the id of the setting to check * @param includeDefaultValue * TRUE to only return false when the setting is explicitly set * to NULL (and not just "no set") in the change maps * * @return TRUE if the setting is set */ protected boolean isSet(String name, boolean includeDefaultValue) { if (getString(name, null) == null) { if (!includeDefaultValue || getString(name, "") == null) { return false; } } return true; } /** * Return the value associated to the given id as a {@link String}. * * @param id * the id of the value to get * * @return the associated value, or NULL if not found (not present in the * resource file) */ public String getString(E id) { return getString(id, null); } /** * Return the value associated to the given id as a {@link String}. *

* If no value is associated, take the default one if any. * * @param id * the id of the value to get * @param def * the default value when it is not present in the config file * * @return the associated value, or def if not found (not present * in the resource file) */ public String getString(E id, String def) { return getString(id, def, -1); } /** * Return the value associated to the given id as a {@link String}. *

* If no value is associated (or if it is empty!), take the default one if * any. * * @param id * the id of the value to get * @param def * the default value when it is not present in the config file * @param item * the item number to get for an array of values, or -1 for * non-arrays * * @return the associated value, def if not found (not present in * the resource file) or NULL if the item is specified (not -1) and * does not exist */ public String getString(E id, String def, int item) { String rep = getString(id.name(), null); if (rep == null) { rep = getMetaDef(id.name()); } if (rep.isEmpty()) { return def; } if (item >= 0) { List values = BundleHelper.parseList(rep, item); if (values != null && item < values.size()) { return values.get(item); } return null; } return rep; } /** * Set the value associated to the given id as a {@link String}. * * @param id * the id of the value to set * @param value * the value * */ public void setString(E id, String value) { setString(id.name(), value); } /** * Set the value associated to the given id as a {@link String}. * * @param id * the id of the value to set * @param value * the value * @param item * the item number to get for an array of values, or -1 for * non-arrays * */ public void setString(E id, String value, int item) { if (item < 0) { setString(id.name(), value); } else { List values = getList(id); setString(id.name(), BundleHelper.fromList(values, value, item)); } } /** * Return the value associated to the given id as a {@link String} suffixed * with the runtime value "_suffix" (that is, "_" and suffix). *

* Will only accept suffixes that form an existing id. *

* If no value is associated, take the default one if any. * * @param id * the id of the value to get * @param suffix * the runtime suffix * * @return the associated value, or NULL if not found (not present in the * resource file) */ public String getStringX(E id, String suffix) { return getStringX(id, suffix, null, -1); } /** * Return the value associated to the given id as a {@link String} suffixed * with the runtime value "_suffix" (that is, "_" and suffix). *

* Will only accept suffixes that form an existing id. *

* If no value is associated, take the default one if any. * * @param id * the id of the value to get * @param suffix * the runtime suffix * @param def * the default value when it is not present in the config file * * @return the associated value, or NULL if not found (not present in the * resource file) */ public String getStringX(E id, String suffix, String def) { return getStringX(id, suffix, def, -1); } /** * Return the value associated to the given id as a {@link String} suffixed * with the runtime value "_suffix" (that is, "_" and suffix). *

* Will only accept suffixes that form an existing id. *

* If no value is associated, take the default one if any. * * @param id * the id of the value to get * @param suffix * the runtime suffix * @param def * the default value when it is not present in the config file * @param item * the item number to get for an array of values, or -1 for * non-arrays * * @return the associated value, def if not found (not present in * the resource file), NULL if the item is specified (not -1) but * does not exist and NULL if bad key */ public String getStringX(E id, String suffix, String def, int item) { String key = id.name() + (suffix == null ? "" : "_" + suffix.toUpperCase()); try { id = Enum.valueOf(type, key); return getString(id, def, item); } catch (IllegalArgumentException e) { } return null; } /** * Set the value associated to the given id as a {@link String} suffixed * with the runtime value "_suffix" (that is, "_" and suffix). *

* Will only accept suffixes that form an existing id. * * @param id * the id of the value to set * @param suffix * the runtime suffix * @param value * the value */ public void setStringX(E id, String suffix, String value) { setStringX(id, suffix, value, -1); } /** * Set the value associated to the given id as a {@link String} suffixed * with the runtime value "_suffix" (that is, "_" and suffix). *

* Will only accept suffixes that form an existing id. * * @param id * the id of the value to set * @param suffix * the runtime suffix * @param value * the value * @param item * the item number to get for an array of values, or -1 for * non-arrays */ public void setStringX(E id, String suffix, String value, int item) { String key = id.name() + (suffix == null ? "" : "_" + suffix.toUpperCase()); try { id = Enum.valueOf(type, key); setString(id, value, item); } catch (IllegalArgumentException e) { } } /** * Return the value associated to the given id as a {@link Boolean}. *

* If no value is associated, take the default one if any. * * @param id * the id of the value to get * * @return the associated value */ public Boolean getBoolean(E id) { return BundleHelper.parseBoolean(getString(id), -1); } /** * Return the value associated to the given id as a {@link Boolean}. *

* If no value is associated, take the default one if any. * * @param id * the id of the value to get * @param def * the default value when it is not present in the config file or * if it is not a boolean value * * @return the associated value */ public boolean getBoolean(E id, boolean def) { Boolean value = getBoolean(id); if (value != null) { return value; } return def; } /** * Return the value associated to the given id as a {@link Boolean}. *

* If no value is associated, take the default one if any. * * @param id * the id of the value to get * @param def * the default value when it is not present in the config file or * if it is not a boolean value * @param item * the item number to get for an array of values, or -1 for * non-arrays * * @return the associated value */ public Boolean getBoolean(E id, boolean def, int item) { String value = getString(id); if (value != null) { return BundleHelper.parseBoolean(value, item); } return def; } /** * Set the value associated to the given id as a {@link Boolean}. * * @param id * the id of the value to set * @param value * the value * */ public void setBoolean(E id, boolean value) { setBoolean(id, value, -1); } /** * Set the value associated to the given id as a {@link Boolean}. * * @param id * the id of the value to set * @param value * the value * @param item * the item number to get for an array of values, or -1 for * non-arrays * */ public void setBoolean(E id, boolean value, int item) { setString(id, BundleHelper.fromBoolean(value), item); } /** * Return the value associated to the given id as an {@link Integer}. *

* If no value is associated, take the default one if any. * * @param id * the id of the value to get * * @return the associated value */ public Integer getInteger(E id) { String value = getString(id); if (value != null) { return BundleHelper.parseInteger(value, -1); } return null; } /** * Return the value associated to the given id as an int. *

* If no value is associated, take the default one if any. * * @param id * the id of the value to get * @param def * the default value when it is not present in the config file or * if it is not a int value * * @return the associated value */ public int getInteger(E id, int def) { Integer value = getInteger(id); if (value != null) { return value; } return def; } /** * Return the value associated to the given id as an int. *

* If no value is associated, take the default one if any. * * @param id * the id of the value to get * @param def * the default value when it is not present in the config file or * if it is not a int value * @param item * the item number to get for an array of values, or -1 for * non-arrays * * @return the associated value */ public Integer getInteger(E id, int def, int item) { String value = getString(id); if (value != null) { return BundleHelper.parseInteger(value, item); } return def; } /** * Set the value associated to the given id as a {@link Integer}. * * @param id * the id of the value to set * @param value * the value * */ public void setInteger(E id, int value) { setInteger(id, value, -1); } /** * Set the value associated to the given id as a {@link Integer}. * * @param id * the id of the value to set * @param value * the value * @param item * the item number to get for an array of values, or -1 for * non-arrays * */ public void setInteger(E id, int value, int item) { setString(id, BundleHelper.fromInteger(value), item); } /** * Return the value associated to the given id as a {@link Character}. *

* If no value is associated, take the default one if any. * * @param id * the id of the value to get * * @return the associated value */ public Character getCharacter(E id) { return BundleHelper.parseCharacter(getString(id), -1); } /** * Return the value associated to the given id as a {@link Character}. *

* If no value is associated, take the default one if any. * * @param id * the id of the value to get * @param def * the default value when it is not present in the config file or * if it is not a char value * * @return the associated value */ public char getCharacter(E id, char def) { Character value = getCharacter(id); if (value != null) { return value; } return def; } /** * Return the value associated to the given id as a {@link Character}. *

* If no value is associated, take the default one if any. * * @param id * the id of the value to get * @param def * the default value when it is not present in the config file or * if it is not a char value * @param item * the item number to get for an array of values, or -1 for * non-arrays * * @return the associated value */ public Character getCharacter(E id, char def, int item) { String value = getString(id); if (value != null) { return BundleHelper.parseCharacter(value, item); } return def; } /** * Set the value associated to the given id as a {@link Character}. * * @param id * the id of the value to set * @param value * the value * */ public void setCharacter(E id, char value) { setCharacter(id, value, -1); } /** * Set the value associated to the given id as a {@link Character}. * * @param id * the id of the value to set * @param value * the value * @param item * the item number to get for an array of values, or -1 for * non-arrays * */ public void setCharacter(E id, char value, int item) { setString(id, BundleHelper.fromCharacter(value), item); } /** * Return the value associated to the given id as a colour if it is found * and can be parsed. *

* The returned value is an ARGB value. *

* If no value is associated, take the default one if any. * * @param id * the id of the value to get * * @return the associated value */ public Integer getColor(E id) { return BundleHelper.parseColor(getString(id), -1); } /** * Return the value associated to the given id as a colour if it is found * and can be parsed. *

* The returned value is an ARGB value. *

* If no value is associated, take the default one if any. * * @param id * the id of the value to get * @param def * the default value when it is not present in the config file or * if it is not a char value * * @return the associated value */ public int getColor(E id, int def) { Integer value = getColor(id); if (value != null) { return value; } return def; } /** * Return the value associated to the given id as a colour if it is found * and can be parsed. *

* The returned value is an ARGB value. *

* If no value is associated, take the default one if any. * * @param id * the id of the value to get * @param def * the default value when it is not present in the config file or * if it is not a char value * @param item * the item number to get for an array of values, or -1 for * non-arrays * * @return the associated value */ public Integer getColor(E id, int def, int item) { String value = getString(id); if (value != null) { return BundleHelper.parseColor(value, item); } return def; } /** * Set the value associated to the given id as a colour. *

* The value is a BGRA value. * * @param id * the id of the value to set * @param color * the new colour */ public void setColor(E id, Integer color) { setColor(id, color, -1); } /** * Set the value associated to the given id as a Color. * * @param id * the id of the value to set * @param value * the value * @param item * the item number to get for an array of values, or -1 for * non-arrays * */ public void setColor(E id, int value, int item) { setString(id, BundleHelper.fromColor(value), item); } /** * Return the value associated to the given id as a list of values if it is * found and can be parsed. *

* If no value is associated, take the default one if any. * * @param id * the id of the value to get * * @return the associated list, empty if the value is empty, NULL if it is * not found or cannot be parsed as a list */ public List getList(E id) { return BundleHelper.parseList(getString(id), -1); } /** * Return the value associated to the given id as a list of values if it is * found and can be parsed. *

* If no value is associated, take the default one if any. * * @param id * the id of the value to get * @param def * the default value when it is not present in the config file or * if it is not a char value * * @return the associated list, empty if the value is empty, NULL if it is * not found or cannot be parsed as a list */ public List getList(E id, List def) { List value = getList(id); if (value != null) { return value; } return def; } /** * Return the value associated to the given id as a list of values if it is * found and can be parsed. *

* If no value is associated, take the default one if any. * * @param id * the id of the value to get * @param def * the default value when it is not present in the config file or * if it is not a char value * @param item * the item number to get for an array of values, or -1 for * non-arrays * * @return the associated list, empty if the value is empty, NULL if it is * not found or cannot be parsed as a list */ public List getList(E id, List def, int item) { String value = getString(id); if (value != null) { return BundleHelper.parseList(value, item); } return def; } /** * Set the value associated to the given id as a list of values. * * @param id * the id of the value to set * @param list * the new list of values */ public void setList(E id, List list) { setList(id, list, -1); } /** * Set the value associated to the given id as a {@link List}. * * @param id * the id of the value to set * @param value * the value * @param item * the item number to get for an array of values, or -1 for * non-arrays * */ public void setList(E id, List value, int item) { setString(id, BundleHelper.fromList(value), item); } /** * Create/update the .properties file. *

* Will use the most likely candidate as base if the file does not already * exists and this resource is translatable (for instance, "en_US" will use * "en" as a base if the resource is a translation file). *

* Will update the files in {@link Bundles#getDirectory()}; it MUST * be set. * * @throws IOException * in case of IO errors */ public void updateFile() throws IOException { updateFile(Bundles.getDirectory()); } /** * Create/update the .properties file. *

* Will use the most likely candidate as base if the file does not already * exists and this resource is translatable (for instance, "en_US" will use * "en" as a base if the resource is a translation file). * * @param path * the path where the .properties files are, MUST NOT be * NULL * * @throws IOException * in case of IO errors */ public void updateFile(String path) throws IOException { File file = getUpdateFile(path); BufferedWriter writer = new BufferedWriter(new OutputStreamWriter( new FileOutputStream(file), "UTF-8")); writeHeader(writer); writer.write("\n"); writer.write("\n"); for (Field field : type.getDeclaredFields()) { Meta meta = field.getAnnotation(Meta.class); if (meta != null) { E id = Enum.valueOf(type, field.getName()); String info = getMetaInfo(meta); if (info != null) { writer.write(info); writer.write("\n"); } writeValue(writer, id); } } writer.close(); } /** * Delete the .properties file. *

* Will use the most likely candidate as base if the file does not already * exists and this resource is translatable (for instance, "en_US" will use * "en" as a base if the resource is a translation file). *

* Will delete the files in {@link Bundles#getDirectory()}; it MUST * be set. * * @return TRUE if the file was deleted */ public boolean deleteFile() { return deleteFile(Bundles.getDirectory()); } /** * Delete the .properties file. *

* Will use the most likely candidate as base if the file does not already * exists and this resource is translatable (for instance, "en_US" will use * "en" as a base if the resource is a translation file). * * @param path * the path where the .properties files are, MUST NOT be * NULL * * @return TRUE if the file was deleted */ public boolean deleteFile(String path) { File file = getUpdateFile(path); return file.delete(); } /** * The description {@link TransBundle}, that is, a {@link TransBundle} * dedicated to the description of the values of the given {@link Bundle} * (can be NULL). * * @return the description {@link TransBundle} */ public TransBundle getDescriptionBundle() { return descriptionBundle; } /** * Reload the {@link Bundle} data files. * * @param resetToDefault * reset to the default configuration (do not look into the * possible user configuration files, only take the original * configuration) */ public void reload(boolean resetToDefault) { setBundle(keyType, Locale.getDefault(), resetToDefault); } /** * Check if the internal map contains the given key. * * @param key * the key to check for * * @return true if it does */ protected boolean containsKey(String key) { return changeMap.containsKey(key) || map.containsKey(key); } /** * The default {@link Meta#def()} value for the given enumeration name. * * @param id * the enumeration name (the "id") * * @return the def value in the {@link MetaInfo} or "" if none (never NULL) */ protected String getMetaDef(String id) { String rep = ""; try { Meta meta = type.getDeclaredField(id).getAnnotation(Meta.class); rep = meta.def(); } catch (NoSuchFieldException e) { } catch (SecurityException e) { } if (rep == null) { rep = ""; } return rep; } /** * Get the value for the given key if it exists in the internal map, or * def if not. *

* DO NOT get the default meta value (MetaInfo.def()). * * @param key * the key to check for * @param def * the default value when it is not present in the internal map * * @return the value, or def if not found */ protected String getString(String key, String def) { if (changeMap.containsKey(key)) { return changeMap.get(key); } if (map.containsKey(key)) { return map.get(key); } return def; } /** * Set the value for this key, in the change map (it is kept in memory, not * yet on disk). * * @param key * the key * @param value * the associated value */ protected void setString(String key, String value) { changeMap.put(key, value == null ? null : value.trim()); } /** * Return formated, display-able information from the {@link Meta} field * given. Each line will always starts with a "#" character. * * @param meta * the {@link Meta} field * * @return the information to display or NULL if none */ protected String getMetaInfo(Meta meta) { String desc = meta.description(); boolean group = meta.group(); Meta.Format format = meta.format(); String[] list = meta.list(); boolean nullable = meta.nullable(); String def = meta.def(); boolean array = meta.array(); // Default, empty values -> NULL if (desc.length() + list.length + def.length() == 0 && !group && nullable && format == Format.STRING) { return null; } StringBuilder builder = new StringBuilder(); for (String line : desc.split("\n")) { builder.append("# ").append(line).append("\n"); } if (group) { builder.append("# This item is used as a group, its content is not expected to be used."); } else { builder.append("# (FORMAT: ").append(format) .append(nullable ? "" : ", required"); builder.append(") "); if (list.length > 0) { builder.append("\n# ALLOWED VALUES: "); boolean first = true; for (String value : list) { if (!first) { builder.append(", "); } builder.append(BundleHelper.escape(value)); first = false; } } if (array) { builder.append("\n# (This item accepts a list of ^escaped comma-separated values)"); } } return builder.toString(); } /** * The display name used in the .properties file. * * @return the name */ protected String getBundleDisplayName() { return keyType.toString(); } /** * Write the header found in the configuration .properties file of * this {@link Bundles}. * * @param writer * the {@link Writer} to write the header in * * @throws IOException * in case of IO error */ protected void writeHeader(Writer writer) throws IOException { writer.write("# " + getBundleDisplayName() + "\n"); writer.write("#\n"); } /** * Write the given data to the config file, i.e., "MY_ID = my_curent_value" * followed by a new line. *

* Will prepend a # sign if the is is not set (see * {@link Bundle#isSet(Enum, boolean)}). * * @param writer * the {@link Writer} to write into * @param id * the id to write * * @throws IOException * in case of IO error */ protected void writeValue(Writer writer, E id) throws IOException { boolean set = isSet(id, false); writeValue(writer, id.name(), getString(id), set); } /** * Write the given data to the config file, i.e., "MY_ID = my_curent_value" * followed by a new line. *

* Will prepend a # sign if the is is not set. * * @param writer * the {@link Writer} to write into * @param id * the id to write * @param value * the id's value * @param set * the value is set in this {@link Bundle} * * @throws IOException * in case of IO error */ protected void writeValue(Writer writer, String id, String value, boolean set) throws IOException { if (!set) { writer.write('#'); } writer.write(id); writer.write(" = "); if (value == null) { value = ""; } String[] lines = value.replaceAll("\t", "\\\\\\t").split("\n"); for (int i = 0; i < lines.length; i++) { writer.write(lines[i]); if (i < lines.length - 1) { writer.write("\\n\\"); } writer.write("\n"); } } /** * Return the source file for this {@link Bundles} from the given path. * * @param path * the path where the .properties files are * * @return the source {@link File} */ protected File getUpdateFile(String path) { return new File(path, keyType.name() + ".properties"); } /** * Change the currently used bundle, and reset all changes. * * @param name * the name of the bundle to load * @param locale * the {@link Locale} to use * @param resetToDefault * reset to the default configuration (do not look into the * possible user configuration files, only take the original * configuration) */ protected void setBundle(Enum name, Locale locale, boolean resetToDefault) { changeMap.clear(); String dir = Bundles.getDirectory(); String bname = type.getPackage().getName() + "." + name.name(); boolean found = false; if (!resetToDefault && dir != null) { try { // Look into Bundles.getDirectory() for .properties files File file = getPropertyFile(dir, name.name(), locale); if (file != null) { InputStream in = new FileInputStream(file); try { Reader reader = new InputStreamReader(in, "UTF-8"); try { resetMap(new PropertyResourceBundle(reader)); } finally { reader.close(); } } finally { in.close(); } found = true; } } catch (IOException e) { e.printStackTrace(); } } if (!found) { // Look into the package itself for resources try { resetMap(ResourceBundle .getBundle(bname, locale, type.getClassLoader(), new FixedResourceBundleControl())); found = true; } catch (MissingResourceException e) { } catch (Exception e) { e.printStackTrace(); } } if (!found) { // We have no bundle for this Bundle System.err.println("No bundle found for: " + bname); resetMap(null); } } /** * Reset the backing map to the content of the given bundle, or with NULL * values if bundle is NULL. * * @param bundle * the bundle to copy */ protected void resetMap(ResourceBundle bundle) { this.map.clear(); if (bundle != null) { for (Field field : type.getDeclaredFields()) { try { Meta meta = field.getAnnotation(Meta.class); if (meta != null) { E id = Enum.valueOf(type, field.getName()); String value = bundle.getString(id.name()); this.map.put(id.name(), value == null ? null : value.trim()); } } catch (MissingResourceException e) { } } } } /** * Take a snapshot of the changes in memory in this {@link Bundle} made by * the "set" methods ( {@link Bundle#setString(Enum, String)}...) at the * current time. * * @return a snapshot to use with {@link Bundle#restoreSnapshot(Object)} */ public Object takeSnapshot() { return new HashMap(changeMap); } /** * Restore a snapshot taken with {@link Bundle}, or reset the current * changes if the snapshot is NULL. * * @param snap * the snapshot or NULL */ @SuppressWarnings("unchecked") public void restoreSnapshot(Object snap) { if (snap == null) { changeMap.clear(); } else { if (snap instanceof Map) { changeMap = (Map) snap; } else { throw new RuntimeException( "Restoring changes in a Bundle must be done on a changes snapshot, " + "or NULL to discard current changes"); } } } /** * Return the resource file that is closer to the {@link Locale}. * * @param dir * the directory to look into * @param name * the file base name (without .properties) * @param locale * the {@link Locale} * * @return the closest match or NULL if none */ private File getPropertyFile(String dir, String name, Locale locale) { List locales = new ArrayList(); if (locale != null) { String country = locale.getCountry() == null ? "" : locale .getCountry(); String language = locale.getLanguage() == null ? "" : locale .getLanguage(); if (!language.isEmpty() && !country.isEmpty()) { locales.add("_" + language + "-" + country); } if (!language.isEmpty()) { locales.add("_" + language); } } locales.add(""); File file = null; for (String loc : locales) { file = new File(dir, name + loc + ".properties"); if (file.exists()) { break; } file = null; } return file; } }