package com.porcupine.util; import java.io.*; import java.util.*; import java.util.Map.Entry; import org.lwjgl.input.Keyboard; import com.porcupine.math.Calc; /** * Property manager with advanced formatting and value checking.
* Methods starting with put are for filling. Most of the others are shortcuts * to getters. * * @author MightyPork */ public class PropertyManager { /** * Properties stored in file, alphabetically sorted.
* Property file is much cleaner than the normal java.util.Properties, * newlines can be inserted to separate categories, and individual keys can * have their own inline comments. * * @author MightyPork * @copy (c) 2012 */ private static class PC_SortedProperties extends Properties { /** A table of hex digits */ private static final char[] hexDigit_custom = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; /** * this is here because the original method is private. * * @param nibble * @return hex char. */ private static char toHex_custom(int nibble) { return hexDigit_custom[(nibble & 0xF)]; } private static void writeComments_custom(BufferedWriter bw, String comm) throws IOException { String comments = comm.replace("\n\n", "\n \n"); int len = comments.length(); int current = 0; int last = 0; char[] uu = new char[6]; uu[0] = '\\'; uu[1] = 'u'; while (current < len) { char c = comments.charAt(current); if (c > '\u00ff' || c == '\n' || c == '\r') { if (last != current) { bw.write("# " + comments.substring(last, current)); } if (c > '\u00ff') { uu[2] = toHex_custom((c >> 12) & 0xf); uu[3] = toHex_custom((c >> 8) & 0xf); uu[4] = toHex_custom((c >> 4) & 0xf); uu[5] = toHex_custom(c & 0xf); bw.write(new String(uu)); } else { bw.newLine(); if (c == '\r' && current != len - 1 && comments.charAt(current + 1) == '\n') { current++; } // if (current == len - 1 || (comments.charAt(current + 1) != '#' && comments.charAt(current + 1) != '!')) // bw.write("#"); } last = current + 1; } current++; } if (last != current) { bw.write("# " + comments.substring(last, current)); } bw.newLine(); bw.newLine(); bw.newLine(); } /** Option: put empty line before each comment. */ public boolean cfgEmptyLineBeforeComment = true; /** * Option: Separate sections by newline
* Section = string before first dot in key. */ public boolean cfgSeparateSectionsByEmptyLine = true; private boolean firstEntry = true; private Hashtable keyComments = new Hashtable(); private String lastSectionBeginning = ""; @SuppressWarnings({ "unchecked", "rawtypes" }) @Override public synchronized Enumeration keys() { Enumeration keysEnum = super.keys(); Vector keyList = new Vector(); while (keysEnum.hasMoreElements()) { keyList.add(keysEnum.nextElement()); } Collections.sort(keyList); return keyList.elements(); } private String saveConvert_custom(String theString, boolean escapeSpace, boolean escapeUnicode) { int len = theString.length(); int bufLen = len * 2; if (bufLen < 0) { bufLen = Integer.MAX_VALUE; } StringBuffer outBuffer = new StringBuffer(bufLen); for (int x = 0; x < len; x++) { char aChar = theString.charAt(x); // Handle common case first, selecting largest block that // avoids the specials below if ((aChar > 61) && (aChar < 127)) { if (aChar == '\\') { outBuffer.append('\\'); outBuffer.append('\\'); continue; } outBuffer.append(aChar); continue; } switch (aChar) { case ' ': if (x == 0 || escapeSpace) { outBuffer.append('\\'); } outBuffer.append(' '); break; case '\t': outBuffer.append('\\'); outBuffer.append('t'); break; case '\n': outBuffer.append('\\'); outBuffer.append('n'); break; case '\r': outBuffer.append('\\'); outBuffer.append('r'); break; case '\f': outBuffer.append('\\'); outBuffer.append('f'); break; case '=': // Fall through case ':': // Fall through case '#': // Fall through case '!': outBuffer.append('\\'); outBuffer.append(aChar); break; default: if (((aChar < 0x0020) || (aChar > 0x007e)) & escapeUnicode) { outBuffer.append('\\'); outBuffer.append('u'); outBuffer.append(toHex_custom((aChar >> 12) & 0xF)); outBuffer.append(toHex_custom((aChar >> 8) & 0xF)); outBuffer.append(toHex_custom((aChar >> 4) & 0xF)); outBuffer.append(toHex_custom(aChar & 0xF)); } else { outBuffer.append(aChar); } } } return outBuffer.toString(); } /** * Set additional comment to a key * * @param key key for comment * @param comment the comment */ public void setKeyComment(String key, String comment) { keyComments.put(key, comment); } @Override public void store(OutputStream out, String comments) throws IOException { store_custom(new BufferedWriter(new OutputStreamWriter(out, "UTF-8")), comments, false); } @SuppressWarnings("rawtypes") private void store_custom(BufferedWriter bw, String comments, boolean escUnicode) throws IOException { if (comments != null) { writeComments_custom(bw, comments); } synchronized (this) { for (Enumeration e = keys(); e.hasMoreElements();) { boolean wasNewLine = false; String key = (String) e.nextElement(); String val = (String) get(key); key = saveConvert_custom(key, true, escUnicode); val = saveConvert_custom(val, false, escUnicode); if (cfgSeparateSectionsByEmptyLine && !lastSectionBeginning.equals(key.split("[.]")[0])) { if (!firstEntry) { bw.newLine(); bw.newLine(); } wasNewLine = true; lastSectionBeginning = key.split("[.]")[0]; } if (keyComments.containsKey(key)) { String cm = keyComments.get(key); cm = cm.replace("\r", "\n"); cm = cm.replace("\r\n", "\n"); cm = cm.replace("\n\n", "\n \n"); String[] cmlines = cm.split("\n"); if (!wasNewLine && !firstEntry && cfgEmptyLineBeforeComment) { bw.newLine(); } for (String cmline : cmlines) { bw.write("# " + cmline); bw.newLine(); } } bw.write(key + " = " + val); bw.newLine(); firstEntry = false; } } bw.flush(); } } /** * Helper class which loads Properties from UTF-8 file (Properties use * "ISO-8859-1" by default) * * @author Itay Maman */ private static class PropertiesLoader { private static String escapifyStr(String str) { StringBuilder result = new StringBuilder(); int len = str.length(); for (int x = 0; x < len; x++) { char ch = str.charAt(x); if (ch <= 0x007e) { result.append(ch); continue; } result.append('\\'); result.append('u'); result.append(hexDigit(ch, 12)); result.append(hexDigit(ch, 8)); result.append(hexDigit(ch, 4)); result.append(hexDigit(ch, 0)); } return result.toString(); } private static char hexDigit(char ch, int offset) { int val = (ch >> offset) & 0xF; if (val <= 9) { return (char) ('0' + val); } return (char) ('A' + val - 10); } public static PC_SortedProperties loadProperties(PC_SortedProperties props, InputStream is) throws IOException { return loadProperties(props, is, "utf-8"); } public static PC_SortedProperties loadProperties(PC_SortedProperties props, InputStream is, String encoding) throws IOException { StringBuilder sb = new StringBuilder(); InputStreamReader isr = new InputStreamReader(is, encoding); while (true) { int temp = isr.read(); if (temp < 0) { break; } char c = (char) temp; sb.append(c); } String read = sb.toString(); String inputString = escapifyStr(read); byte[] bs = inputString.getBytes("ISO-8859-1"); ByteArrayInputStream bais = new ByteArrayInputStream(bs); PC_SortedProperties ps = props; ps.load(bais); return ps; } } /** * Property entry in Property manager. * * @author MightyPork * @copy (c) 2012 */ private class Property { public boolean bool = false; public String entryComment; public boolean defbool = false; public double defnum = -1; public String defstr = ""; public String name; public double num = -1; public String str = ""; public PropertyType type; /** * Property * * @param key key * @param default_value default value * @param entry_type type * @param entry_comment entry comment */ public Property(String key, boolean default_value, PropertyType entry_type, String entry_comment) { name = key; defbool = default_value; type = entry_type; entryComment = entry_comment; } /** * Property entry * * @param key property key * @param default_value default value * @param entry_type property type from enum * @param entry_comment property comment or null */ public Property(String key, double default_value, PropertyType entry_type, String entry_comment) { name = key; defnum = default_value; type = entry_type; entryComment = entry_comment; } /** * Property * * @param key key * @param default_value default value * @param entry_type type * @param entry_comment entry comment */ public Property(String key, String default_value, PropertyType entry_type, String entry_comment) { name = key; defstr = default_value; type = entry_type; entryComment = entry_comment; } /** * Get boolean * * @return the boolean */ public boolean getBoolean() { return bool; } /** * Get number * * @return the number */ public int getInteger() { return (int) Math.round(num); } /** * Get number as double * * @return the number */ public double getDouble() { return num; } /** * Get string * * @return the string */ public String getString() { return str; } /** * is this key pressed? * * @return pressed state */ public boolean isKeyDown() { return type == PropertyType.KEY && Keyboard.isKeyDown((int) num); } /** * Is this entry valid? * * @return is valid */ public boolean isValid() { if (type == PropertyType.KEY) { return Keyboard.getKeyName((int) num) != null; } if (type == PropertyType.STRING) { return str != null; } if (type == PropertyType.BOOLEAN || type == PropertyType.INT || type == PropertyType.DOUBLE) { return true; } return false; } /** * Load property value from a file * * @param string the string loaded * @return this entry */ public boolean parse(String string) { if (type == PropertyType.INT) { if (string == null) { //Log.finest("* Numeric property \"" + name + "\" not set, setting to default \"" + defnum + "\""); num = defnum; return false; } try { num = Integer.parseInt(string.trim()); } catch (NumberFormatException e) { //Log.warning("Numeric property \"" + name + "\" has invalid value \"" + string + "\". Falling back to default \"" + defnum + "\""); num = defnum; } } if (type == PropertyType.DOUBLE) { if (string == null) { //Log.finest("* Numeric property \"" + name + "\" not set, setting to default \"" + defnum + "\""); num = defnum; return false; } try { num = Double.parseDouble(string.trim()); } catch (NumberFormatException e) { //Log.warning("Numeric property \"" + name + "\" has invalid value \"" + string + "\". Falling back to default \"" + defnum + "\""); num = defnum; } } if (type == PropertyType.KEY) { if (string == null) { //Log.finest("* Key property \"" + name + "\" not set, setting to default \"" + Keyboard.getKeyName(defnum) + "\""); num = defnum; return false; } num = Keyboard.getKeyIndex(string); if (num == Keyboard.KEY_NONE) { //Log.warning("Key property \"" + name + "\" has invalid value \"" + string + "\". Falling back to default \"" // + Keyboard.getKeyName(defnum) + "\""); num = defnum; } } if (type == PropertyType.STRING) { if (string == null) { //Log.finest("* String property \"" + name + "\" not set, setting to default \"" + defstr + "\""); str = defstr; return false; } this.str = string; } if (type == PropertyType.BOOLEAN) { if (string == null) { //Log.finest("* Boolean property \"" + name + "\" not set, setting to default \"" + defbool + "\""); bool = defbool; return false; } String string2 = string.toLowerCase(); bool = string2.equals("yes") || string2.equals("true") || string2.equals("on") || string2.equals("enabled") || string2.equals("enable"); } return true; } /** * prepare the contents for insertion into Properties * * @return the string prepared, or null if type is invalid */ @Override public String toString() { if (!isValid()) { if (type == PropertyType.INT|| type == PropertyType.DOUBLE) { num = defnum; } } if (type == PropertyType.INT) { return Integer.toString((int) num); } if (type == PropertyType.DOUBLE) { return Calc.floatToString((float) num); } if (type == PropertyType.STRING) { return str; } if (type == PropertyType.KEY) { return Keyboard.getKeyName((int) num) == null ? "none" : Keyboard.getKeyName((int) num); } if (type == PropertyType.BOOLEAN) { return bool ? "True" : "False"; } return null; } /** * If this entry is not valid, change it to the dafault value. */ public void validate() { if (!isValid()) { if (type == PropertyType.KEY) { //Log.warning("Key property \"" + name + "\" has invalid value (unknown key name). Falling back to default value \"" // + Keyboard.getKeyName(defnum) + "\""); num = defnum; } if (type == PropertyType.STRING) { //Log.warning("String property \"" + name + "\" has invalid value (NULL). Falling back to default value \"" + defstr + "\""); str = defstr; } } } } /** * Property type enum. * * @author MightyPork * @copy (c) 2012 */ private enum PropertyType { BOOLEAN, INT, KEY, STRING, DOUBLE; } /** * Option to put newline before inline comments */ private boolean cfgNewlineBeforeComments = true; /** Disable entry validation */ private boolean cfgNoValidate = true; /** * Option to put newline between sections.
* Sections are detected by text before first dot in identifier. */ private boolean cfgSeparateSections = true; /** * Disables enter-leave logging. */ private boolean cfgSilent = false; private String comment = ""; private TreeMap entries; private String filename; private TreeMap keyRename; private PC_SortedProperties pr = new PC_SortedProperties(); private TreeMap setValues; /** * Create property manager from file path and an initial comment. * * @param filename file with the props * @param comment the initial comment. Use \n in it if you want. */ public PropertyManager(String filename, String comment) { this.filename = filename; this.entries = new TreeMap(); this.setValues = new TreeMap(); this.keyRename = new TreeMap(); this.comment = comment; } /** * Load, fix and write to file. */ public void apply() { if (!cfgSilent) { //Log.finest("Loading configuration from file \"" + filename + "\""); } boolean needsSave = false; try { new File((new File(filename)).getParent()).mkdirs(); pr = PropertiesLoader.loadProperties(pr, new FileInputStream(filename)); } catch (IOException e) { needsSave = true; pr = new PC_SortedProperties(); } pr.cfgSeparateSectionsByEmptyLine = cfgSeparateSections; pr.cfgEmptyLineBeforeComment = cfgNewlineBeforeComments; ArrayList keyList = new ArrayList(); // rename keys for (Entry entry : keyRename.entrySet()) { if (pr.getProperty(entry.getKey()) == null) { continue; } pr.setProperty(entry.getValue(), pr.getProperty(entry.getKey())); pr.remove(entry.getKey()); needsSave = true; } // set the override values into the freshly loaded properties file for (Entry entry : setValues.entrySet()) { pr.setProperty(entry.getKey(), entry.getValue()); needsSave = true; } // validate entries one by one, replace with default when needed for (Property entry : entries.values()) { keyList.add(entry.name); String propOrig = pr.getProperty(entry.name); if (!entry.parse(propOrig)) needsSave = true; if (!cfgNoValidate) { entry.validate(); } if (entry.entryComment != null) { pr.setKeyComment(entry.name, entry.entryComment); } if (propOrig == null || !entry.toString().equals(propOrig)) { pr.setProperty(entry.name, entry.toString()); needsSave = true; } } // removed unused props for (String propname : pr.keySet().toArray(new String[pr.size()])) { if (!keyList.contains(propname)) { pr.remove(propname); //Log.finest("* Removing unused property \"" + propname + "\" from config file " + filename); needsSave = true; } } // save if needed if (needsSave) { try { //Log.finest("* Saving modified property file " + filename); pr.store(new FileOutputStream(filename), comment); } catch (IOException ioe) { ioe.printStackTrace(); } } if (!cfgSilent) { //Log.finest("Configuration loaded."); } setValues.clear(); keyRename.clear(); } /** * Get boolean property * * @param n key * @return the boolean found, or false */ public Boolean bool(String n) { return getBoolean(n); } /** * @param newlineBeforeComments put newline before comments */ public void cfgNewlineBeforeComments(boolean newlineBeforeComments) { this.cfgNewlineBeforeComments = newlineBeforeComments; } /** * @param separateSections do separate sections by newline */ public void cfgSeparateSections(boolean separateSections) { this.cfgSeparateSections = separateSections; } /** * @param silent the cfgSilent to set */ public void cfgSilent(boolean silent) { this.cfgSilent = silent; } /** * @param validate enable validation */ public void enableValidation(boolean validate) { this.cfgNoValidate = !validate; } /** * Get boolean property * * @param n key * @return the boolean found, or false */ public Boolean flag(String n) { return getBoolean(n); } /** * Get a property entry (rarely used) * * @param n key * @return the entry */ private Property get(String n) { try { return entries.get(n); } catch (Throwable t) { return null; } } /** * Get boolean property * * @param n key * @return the boolean found, or false */ public Boolean getBoolean(String n) { try { return entries.get(n).getBoolean(); } catch (Throwable t) { return false; } } /** * Get numeric property * * @param n key * @return the int found, or null */ public Integer getInt(String n) { return getInteger(n); } /** * Get numeric property * * @param n key * @return the int found, or null */ public Integer getInteger(String n) { try { return get(n).getInteger(); } catch (Throwable t) { return -1; } } /** * Get numeric property as double * * @param n key * @return the double found, or null */ public Double getDouble(String n) { try { return get(n).getDouble(); } catch (Throwable t) { return -1D; } } /** * Get numeric property * * @param n key * @return the int found, or null */ public Integer getNum(String n) { return getInteger(n); } /** * Get string property * * @param n key * @return the string found, or null */ public String getString(String n) { try { return get(n).getString(); } catch (Throwable t) { return null; } } /** * Get numeric property * * @param n key * @return the int found, or null */ public Integer integer(String n) { try { return get(n).getInteger(); } catch (Throwable t) { return -1; } } /** * Is the key pressed? (works only for properties of type KEY) * * @param n key of the key property * @return is pressed */ public Boolean isKeyDown(String n) { try { return entries.get(n).isKeyDown(); } catch (Throwable t) { return false; } } /** * Get numeric property * * @param n key * @return the int found, or null */ public Integer num(String n) { return getInteger(n); } /** * Add a boolean property * * @param n key * @param d default value */ public void putBoolean(String n, boolean d) { entries.put(n, new Property(n, d, PropertyType.BOOLEAN, null)); return; } /** * Add a boolean property * * @param n key * @param d default value * @param comment the in-file comment */ public void putBoolean(String n, boolean d, String comment) { entries.put(n, new Property(n, d, PropertyType.BOOLEAN, comment)); return; } /** * Add a numeric property (double) * * @param n key * @param d default value */ public void putDouble(String n, int d) { entries.put(n, new Property(n, d, PropertyType.DOUBLE, null)); return; } /** * Add a numeric property (double) * * @param n key * @param d default value * @param comment the in-file comment */ public void putDouble(String n, int d, String comment) { entries.put(n, new Property(n, d, PropertyType.DOUBLE, comment)); return; } /** * Add a numeric property * * @param n key * @param d default value */ public void putInteger(String n, int d) { entries.put(n, new Property(n, d, PropertyType.INT, null)); return; } /** * Add a numeric property * * @param n key * @param d default value * @param comment the in-file comment */ public void putInteger(String n, int d, String comment) { entries.put(n, new Property(n, d, PropertyType.INT, comment)); return; } /** * Add a numeric property * * @param n key * @param d default value */ public void putKey(String n, int d) { entries.put(n, new Property(n, d, PropertyType.KEY, null)); return; } /** * Add a numeric property * * @param n key * @param d default value * @param comment the in-file comment */ public void putKey(String n, int d, String comment) { entries.put(n, new Property(n, d, PropertyType.KEY, comment)); return; } /** * Add a string property * * @param n key * @param d default value */ public void putString(String n, String d) { entries.put(n, new Property(n, d, PropertyType.STRING, null)); return; } /** * Add a string property * * @param n key * @param d default value * @param comment the in-file comment */ public void putString(String n, String d, String comment) { entries.put(n, new Property(n, d, PropertyType.STRING, comment)); return; } /** * Rename key before doing "apply"; value is preserved * * @param oldKey old key * @param newKey new key */ public void renameKey(String oldKey, String newKey) { keyRename.put(oldKey, newKey); return; } /** * Set value saved to certain key; use to save runtime-changed configuration * values. * * @param key key * @param value the saved value */ public void setValue(String key, Object value) { setValues.put(key, value.toString()); return; } /** * Get string property * * @param n key * @return the string found, or null */ public String str(String n) { try { return get(n).getString(); } catch (Throwable t) { return null; } } /** * Get string property * * @param n key * @return the string found, or null */ public String string(String n) { try { return get(n).getString(); } catch (Throwable t) { return null; } } }