Rogue: Savage Rats, a retro-themed dungeon crawler
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
rogue-savage-rats/src/mightypork/utils/files/PropertyManager.java

1015 lines
20 KiB

package mightypork.utils.files;
import java.io.*;
import java.util.*;
import java.util.Map.Entry;
import mightypork.utils.math.Calc;
/**
* Property manager with advanced formatting and value checking.<br>
* 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.<br>
* 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
*/
private static class SortedProperties extends Properties {
/** A table of hex digits */
private static final char[] hexChars = { '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(int nibble)
{
return hexChars[(nibble & 0xF)];
}
private static void writeComments(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((c >> 12) & 0xf);
uu[3] = toHex((c >> 8) & 0xf);
uu[4] = toHex((c >> 4) & 0xf);
uu[5] = toHex(c & 0xf);
bw.write(new String(uu));
} else {
bw.newLine();
if (c == '\r' && current != len - 1 && comments.charAt(current + 1) == '\n') {
current++;
}
}
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<br>
* Section = string before first dot in key.
*/
public boolean cfgSeparateSectionsByEmptyLine = true;
private boolean firstEntry = true;
/** Comments for individual keys */
private Hashtable<String, String> keyComments = new Hashtable<String, String>();
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(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((aChar >> 12) & 0xF));
outBuffer.append(toHex((aChar >> 8) & 0xF));
outBuffer.append(toHex((aChar >> 4) & 0xF));
outBuffer.append(toHex(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);
}
@SuppressWarnings("rawtypes")
@Override
public void store(OutputStream out, String comments) throws IOException
{
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(out, "UTF-8"));
boolean escUnicode = false;
if (comments != null) {
writeComments(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(key, true, escUnicode);
val = saveConvert(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 SortedProperties loadProperties(SortedProperties props, InputStream is) throws IOException
{
return loadProperties(props, is, "utf-8");
}
public static SortedProperties loadProperties(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);
SortedProperties ps = props;
ps.load(bais);
return ps;
}
}
/**
* Property entry in Property manager.
*
* @author MightyPork
*/
private class Property {
public String entryComment;
public String name;
public boolean bool = false;
public boolean defbool = false;
public double num = -1;
public double defnum = -1;
public String defstr = "";
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 entry valid?
*
* @return is valid
*/
public boolean isValid()
{
switch (type) {
case STRING:
return str != null;
case BOOLEAN:
case INT:
case DOUBLE:
return true;
}
return false;
}
/**
* Load property value from a file
*
* @param string
* the string loaded
* @return was OK
*/
public boolean parse(String string)
{
switch (type) {
case INT:
if (string == null) {
num = defnum;
return false;
}
try {
num = Integer.parseInt(string.trim());
} catch (NumberFormatException e) {
num = defnum;
}
break;
case DOUBLE:
if (string == null) {
num = defnum;
return false;
}
try {
num = Double.parseDouble(string.trim());
} catch (NumberFormatException e) {
num = defnum;
}
break;
case STRING:
if (string == null) {
str = defstr;
return false;
}
str = string;
break;
case BOOLEAN:
if (string == null) {
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;
}
}
switch (type) {
case INT:
return Integer.toString((int) num);
case DOUBLE:
return Calc.floatToString((float) num);
case STRING:
return str;
case 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.STRING) {
str = defstr;
}
}
}
}
/**
* Property types
*/
private enum PropertyType {
BOOLEAN, INT, STRING, DOUBLE;
}
/** put newline before entry comments */
private boolean cfgNewlineBeforeComments = true;
/** Disable entry validation */
private boolean cfgNoValidate = true;
/** Put newline between sections. */
private boolean cfgSeparateSections = true;
/** Force save, even if nothing changed (used to save changed comments) */
private boolean cfgForceSave;
private File file;
private String fileComment = "";
private TreeMap<String, Property> entries;
private TreeMap<String, String> keyRename;
private TreeMap<String, String> setValues;
private SortedProperties pr = new SortedProperties();
/**
* Create property manager from file path and an initial comment.
*
* @param file
* file with the props
* @param comment
* the initial comment. Use \n in it if you want.
*/
public PropertyManager(File file, String comment) {
this.file = file;
this.entries = new TreeMap<String, Property>();
this.setValues = new TreeMap<String, String>();
this.keyRename = new TreeMap<String, String>();
this.fileComment = comment;
}
/**
* Load, fix and write to file.
*/
public void apply()
{
boolean needsSave = false;
FileInputStream fis = null;
try {
new File(file.getParent()).mkdirs();
fis = new FileInputStream(file);
pr = PropertiesLoader.loadProperties(pr, fis);
} catch (IOException e) {
needsSave = true;
pr = new SortedProperties();
} finally {
try {
if (fis != null) fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
pr.cfgSeparateSectionsByEmptyLine = cfgSeparateSections;
pr.cfgEmptyLineBeforeComment = cfgNewlineBeforeComments;
ArrayList<String> keyList = new ArrayList<String>();
// rename keys
for (Entry<String, String> 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<String, String> 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);
needsSave = true;
}
}
// save if needed
if (needsSave || cfgForceSave) {
try {
pr.store(new FileOutputStream(file), fileComment);
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
setValues.clear();
keyRename.clear();
}
/**
* @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 forceSave
* save even if unchanged.
*/
public void cfgForceSave(boolean forceSave)
{
this.cfgForceSave = forceSave;
}
/**
* @param validate
* enable validation
*/
public void enableValidation(boolean validate)
{
this.cfgNoValidate = !validate;
}
/**
* 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 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 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;
}
}
/**
* 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 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;
}
}