X-Git-Url: http://git.nikiroo.be/?a=blobdiff_plain;f=src%2Fbe%2Fnikiroo%2Futils%2Fresources%2FBundle.java;h=1dbb251f942b4d3a95d0720e29a8ddcdc48a444e;hb=refs%2Ftags%2Fnikiroo-utils-2.0.0;hp=78d73a8148088619180caa59aa50fffcc2d50d40;hpb=2cce3dcb72c55aa5cca66e1398f23906e286abc8;p=nikiroo-utils.git diff --git a/src/be/nikiroo/utils/resources/Bundle.java b/src/be/nikiroo/utils/resources/Bundle.java index 78d73a8..1dbb251 100644 --- a/src/be/nikiroo/utils/resources/Bundle.java +++ b/src/be/nikiroo/utils/resources/Bundle.java @@ -1,5 +1,6 @@ package be.nikiroo.utils.resources; +import java.awt.Color; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; @@ -11,60 +12,100 @@ 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; /** - * This class encapsulate a {@link ResourceBundle} in UTF-8. It only allows to + * This class encapsulate a {@link ResourceBundle} in UTF-8. It allows to * retrieve values associated to an enumeration, and allows some additional * methods. - * - * @author niki + *

+ * 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; - protected Enum name; - private ResourceBundle map; + /** + * 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) { + protected Bundle(Class type, Enum name, + TransBundle descriptionBundle) { this.type = type; - this.name = name; - setBundle(name, Locale.getDefault()); + this.keyType = name; + this.descriptionBundle = descriptionBundle; + + this.map = new HashMap(); + this.changeMap = new HashMap(); + setBundle(name, Locale.getDefault(), false); } /** * Return the value associated to the given id as a {@link String}. * - * @param mame + * @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 getStringX(id, ""); + return getString(id.name()); + } + + /** + * Set the value associated to the given id as a {@link String}. + * + * @param id + * the id of the value to get + * @param value + * the value + * + */ + public void setString(E id, String value) { + setString(id.name(), value); } /** * 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. * - * @param mame + * @param id * the id of the value to get * @param suffix * the runtime suffix @@ -74,20 +115,47 @@ public class Bundle> { */ public String getStringX(E id, String suffix) { String key = id.name() - + ((suffix == null || suffix.isEmpty()) ? "" : "_" - + suffix.toUpperCase()); + + (suffix == null ? "" : "_" + suffix.toUpperCase()); + + try { + id = Enum.valueOf(type, key); + return getString(id); + } catch (IllegalArgumentException e) { - if (containsKey(key)) { - return getString(key).trim(); } 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 get + * @param suffix + * the runtime suffix + * @param value + * the value + */ + public void setStringX(E id, String suffix, String value) { + String key = id.name() + + (suffix == null ? "" : "_" + suffix.toUpperCase()); + + try { + id = Enum.valueOf(type, key); + setString(id, value); + } catch (IllegalArgumentException e) { + + } + } + /** * Return the value associated to the given id as a {@link Boolean}. * - * @param mame + * @param id * the id of the value to get * * @return the associated value @@ -108,9 +176,9 @@ public class Bundle> { } /** - * Return the value associated to the given id as a {@link boolean}. + * Return the value associated to the given id as a {@link Boolean}. * - * @param mame + * @param id * the id of the value to get * @param def * the default value when it is not present in the config file or @@ -129,7 +197,7 @@ public class Bundle> { /** * Return the value associated to the given id as an {@link Integer}. * - * @param mame + * @param id * the id of the value to get * * @return the associated value @@ -144,9 +212,9 @@ public class Bundle> { } /** - * Return the value associated to the given id as a {@link int}. + * Return the value associated to the given id as an int. * - * @param mame + * @param id * the id of the value to get * @param def * the default value when it is not present in the config file or @@ -165,28 +233,140 @@ public class Bundle> { /** * Return the value associated to the given id as a {@link Character}. * - * @param mame + * @param id * the id of the value to get * * @return the associated value */ - public char getChar(E id) { + public Character getCharacter(E id) { String s = getString(id).trim(); if (s.length() > 0) { return s.charAt(0); } - return ' '; + return null; + } + + /** + * Return the value associated to the given id as a {@link Character}. + * + * @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) { + String s = getString(id).trim(); + if (s.length() > 0) { + return s.charAt(0); + } + + return def; + } + + /** + * Return the value associated to the given id as a {@link Color}. + * + * @param id + * the id of the value to get + * + * @return the associated value + */ + public Color getColor(E id) { + Color color = null; + + String bg = getString(id).trim(); + if (bg.startsWith("#") && (bg.length() == 7 || bg.length() == 9)) { + try { + int r = Integer.parseInt(bg.substring(1, 3), 16); + int g = Integer.parseInt(bg.substring(3, 5), 16); + int b = Integer.parseInt(bg.substring(5, 7), 16); + int a = 255; + if (bg.length() == 9) { + a = Integer.parseInt(bg.substring(7, 9), 16); + } + color = new Color(r, g, b, a); + } catch (NumberFormatException e) { + color = null; // no changes + } + } + + // Try by name if still not found + if (color == null) { + try { + Field field = Color.class.getField(bg); + color = (Color) field.get(null); + } catch (Exception e) { + } + } + // + + return color; + } + + /** + * Set the value associated to the given id as a {@link Color}. + * + * @param id + * the id of the value to set + * @param color + * the new color + */ + public void setColor(E id, Color color) { + // Check for named colours first + try { + Field[] fields = Color.class.getFields(); + for (Field field : fields) { + if (field.equals(color)) { + setString(id, field.getName()); + return; + } + } + } catch (Exception e) { + } + // + + String r = Integer.toString(color.getRed(), 16); + String g = Integer.toString(color.getGreen(), 16); + String b = Integer.toString(color.getBlue(), 16); + String a = ""; + if (color.getAlpha() < 255) { + a = Integer.toString(color.getAlpha(), 16); + } + + setString(id, "#" + r + g + b + a); + } + + /** + * 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). + * 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 + * the path where the .properties files are, MUST NOT be + * NULL * * @throws IOException * in case of IO errors @@ -219,6 +399,29 @@ public class Bundle> { writer.close(); } + /** + * 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. * @@ -228,12 +431,7 @@ public class Bundle> { * @return true if it does */ protected boolean containsKey(String key) { - try { - map.getObject(key); - return true; - } catch (MissingResourceException e) { - return false; - } + return changeMap.containsKey(key) || map.containsKey(key); } /** @@ -246,13 +444,30 @@ public class Bundle> { * @return the value, or NULL */ protected String getString(String key) { - if (containsKey(key)) { - return map.getString(key); + if (changeMap.containsKey(key)) { + return changeMap.get(key); + } + + if (map.containsKey(key)) { + return map.get(key); } return null; } + /** + * 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. @@ -263,44 +478,45 @@ public class Bundle> { * @return the information to display or NULL if none */ protected String getMetaInfo(Meta meta) { - String what = meta.what(); - String where = meta.where(); - String format = meta.format(); + String desc = meta.description(); + boolean group = meta.group(); + Meta.Format format = meta.format(); + String[] list = meta.list(); + boolean nullable = meta.nullable(); String info = meta.info(); + boolean array = meta.array(); - int opt = what.length() + where.length() + format.length(); - if (opt + info.length() == 0) + // Default, empty values -> NULL + if (desc.length() + list.length + info.length() == 0 && !group + && nullable && format == Meta.Format.STRING) { return null; + } StringBuilder builder = new StringBuilder(); - builder.append("# "); - - if (opt > 0) { - builder.append("("); - if (what.length() > 0) { - builder.append("WHAT: " + what); - if (where.length() + format.length() > 0) - builder.append(", "); - } - - if (where.length() > 0) { - builder.append("WHERE: " + where); - if (format.length() > 0) - builder.append(", "); - } + builder.append("# ").append(desc); + if (desc.length() > 20) { + builder.append("\n#"); + } - if (format.length() > 0) { - builder.append("FORMAT: " + format); + 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(") ").append(info); + + if (list.length > 0) { + builder.append("\n# ALLOWED VALUES:"); + for (String value : list) { + builder.append(" \"").append(value).append("\""); + } } - builder.append(")"); - if (info.length() > 0) { - builder.append("\n# "); + if (array) { + builder.append("\n# (This item accept a list of comma-separated values)"); } } - builder.append(info); - return builder.toString(); } @@ -310,7 +526,7 @@ public class Bundle> { * @return the name */ protected String getBundleDisplayName() { - return name.toString(); + return keyType.toString(); } /** @@ -367,7 +583,7 @@ public class Bundle> { value = ""; } - String[] lines = value.replaceAll("\\\t", "\\\\\\t").split("\n"); + 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) { @@ -384,42 +600,108 @@ public class Bundle> { * the path where the .properties files are * * @return the source {@link File} - * - * @throws IOException - * in case of IO errors */ protected File getUpdateFile(String path) { - return new File(path, name.name() + ".properties"); + return new File(path, keyType.name() + ".properties"); } /** - * Change the currently used bundle. + * 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) { - map = null; + protected void setBundle(Enum name, Locale locale, boolean resetToDefault) { + changeMap.clear(); String dir = Bundles.getDirectory(); - if (dir != null) { + boolean found = false; + if (!resetToDefault && dir != null) { try { File file = getPropertyFile(dir, name.name(), locale); if (file != null) { Reader reader = new InputStreamReader(new FileInputStream( file), "UTF8"); - map = new PropertyResourceBundle(reader); + resetMap(new PropertyResourceBundle(reader)); + found = true; } } catch (IOException e) { e.printStackTrace(); } } - if (map == null) { - map = ResourceBundle.getBundle(type.getPackage().getName() + "." - + name.name(), locale, new FixedResourceBundleControl()); + if (!found) { + String bname = type.getPackage().getName() + "." + name.name(); + try { + resetMap(ResourceBundle + .getBundle(bname, locale, type.getClassLoader(), + new FixedResourceBundleControl())); + } catch (Exception e) { + // 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 empty if + * bundle is NULL. + * + * @param bundle + * the bundle to copy + */ + protected void resetMap(ResourceBundle bundle) { + this.map.clear(); + + if (bundle != null) { + for (E field : type.getEnumConstants()) { + try { + String value = bundle.getString(field.name()); + this.map.put(field.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 Error( + "Restoring changes in a Bundle must be done on a changes snapshot, " + + "or NULL to discard current changes"); + } } } @@ -427,9 +709,9 @@ public class Bundle> { * Return the resource file that is closer to the {@link Locale}. * * @param dir - * the dirctory to look into + * the directory to look into * @param name - * the file basename (without .properties) + * the file base name (without .properties) * @param locale * the {@link Locale} *