package chatty.util.settings;
import chatty.Logging;
import chatty.util.MiscUtil;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map.Entry;
import java.util.*;
import java.util.logging.Logger;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
/**
* Manage (add/change/get/save/load) settings.
*
* Reading and writing values should hopefully maybe be thread-safe. Adding
* settings and checking type isn't synchronized, but if settings are only
* added once at the beginning this shouldn't be a problem.
*
* @author tduva
*/
public class Settings {
private final Object LOCK = new Object();
/**
* Holds all settings of different Types. TreeMap to have setting names
* lookup case-insenstive while still retaining the case for display.
*/
private final Map<String,Setting> settings = new TreeMap(String.CASE_INSENSITIVE_ORDER);
private final Set<SettingChangeListener> listeners = new HashSet<>();
private final Set<SettingsListener> settingsListeners = new HashSet<>();
private final String defaultFile;
private final Set<String> files = new HashSet<>();
private static final Logger LOGGER = Logger.getLogger(Settings.class.getName());
private static final Charset CHARSET = Charset.forName("UTF-8");
private boolean saved;
public Settings(String path) {
this.defaultFile = path;
}
public void addSettingChangeListener(SettingChangeListener listener) {
listeners.add(listener);
}
private void settingChanged(String setting, int type, Object value) {
for (SettingChangeListener listener : listeners) {
listener.settingChanged(setting, type, value);
}
}
public void addSettingsListener(SettingsListener listener) {
settingsListeners.add(listener);
}
private void aboutToSaveSettings() {
for (SettingsListener listener : settingsListeners) {
listener.aboutToSaveSettings(this);
}
}
public void addFile(String fileName) {
files.add(fileName);
}
public void setFile(String settingName, String fileName) {
if (!isSetting(settingName)) {
throw new SettingNotFoundException("Could not find setting: "+settingName);
}
if (!files.contains(fileName)) {
throw new SettingFileNotFoundException("Could not find setting file: "+fileName);
}
Setting setting = settings.get(settingName);
setting.setFile(fileName);
}
private boolean isSetting(String settingName) {
if (getType(settingName) != Setting.UNDEFINED) {
return true;
}
return false;
}
/**
* Checks if the setting with the given name is of the given type.
*
* Returns true if the settings exists and is of the given type, false
* otherwise.
*
* @param settingName The name of the setting to check
* @param type
* @return
*/
private boolean isOfType(String settingName, int type) {
Setting obj = settings.get(settingName);
if (obj != null && obj.isOfType(type)) {
return true;
}
return false;
}
private boolean isOfSubtype(String settingName, int type) {
Setting obj = settings.get(settingName);
if (obj != null && obj instanceof SubtypeSetting) {
return ((SubtypeSetting)obj).isOfSubType(type);
}
return false;
}
private int getType(String settingName) {
Setting obj = settings.get(settingName);
if (obj != null) {
return obj.getType();
}
return Setting.UNDEFINED;
}
public boolean isBooleanSetting(String settingName) {
return isOfType(settingName, Setting.BOOLEAN);
}
public boolean isStringSetting(String settingName) {
return isOfType(settingName, Setting.STRING);
}
public boolean isLongSetting(String settingName) {
return isOfType(settingName, Setting.LONG);
}
public boolean isMapSetting(String settingName) {
return isOfType(settingName, Setting.MAP);
}
public boolean isListSetting(String settingName) {
return isOfType(settingName, Setting.LIST);
}
public void addBoolean(String settingName, boolean value, boolean save) {
add(settingName, value, Setting.BOOLEAN, Setting.UNDEFINED, save);
}
public void addString(String settingName, String value, boolean save) {
add(settingName, value, Setting.STRING, Setting.UNDEFINED, save);
}
public void addLong(String settingName, long value, boolean save) {
add(settingName, value, Setting.LONG, Setting.UNDEFINED, save);
}
public void addBoolean(String settingName, boolean value) {
addBoolean(settingName, value, true);
}
public void addString(String settingName, String value) {
addString(settingName, value, true);
}
public void addLong(String settingName, long value) {
addLong(settingName, value, true);
}
public void addMap(String settingName, Map value, int type) {
add(settingName, value, Setting.MAP, type, true);
}
public void addList(String settingName, Collection value, int type) {
add(settingName, value, Setting.LIST, type, true);
}
public int setString(String settingName, String value) {
return set(settingName, value, Setting.STRING);
}
public int setBoolean(String settingName, boolean value) {
return set(settingName, value, Setting.BOOLEAN);
}
public int setLong(String settingName, long value) {
return set(settingName, value, Setting.LONG);
}
/**
* Adds a setting, overwrites existing setting. A setting must be added
* before changing the value or getting the setting is possible.
*
* @param settingName
* @param value
* @param type
* @param save
*/
private void add(String settingName, Object value, int type, int subType, boolean save) {
if (type == Setting.MAP || type == Setting.LIST) {
settings.put(settingName, new SubtypeSetting(value, type, subType, save, defaultFile));
}
else {
settings.put(settingName, new Setting(value, type, save, defaultFile));
}
}
/**
* Sets the given setting to a new value. The type must match the type
* of the given value.
*
* @param settingName
* @param value
* @param type
*/
private int set(String settingName, Object value, int type) {
boolean changed = false;
synchronized(LOCK) {
if (!isOfType(settingName, type)) {
throw new SettingNotFoundException("Could not find setting: " + settingName);
}
Setting setting = settings.get(settingName);
if (value == null) {
changed = setting.setToDefault();
value = setting.getValue();
} else {
changed = setting.setValue(value);
}
}
if (changed) {
settingChanged(settingName,type,value);
return Setting.CHANGED;
}
return Setting.NOT_CHANGED;
}
/**
* Gets the value (as Object, but containing the type specified by the type
* parameter) of the setting with the given name and type, or throws an
* exception if the setting wasn't found or wasn't of the specified type.
*
* @param settingName The name of the setting.
* @param type The type of the setting.
* @return The Object value, which is actually of 'type'.
*/
private Object get(String settingName, int type) {
synchronized(LOCK) {
if (!isOfType(settingName, type)) {
throw new SettingNotFoundException("Could not find setting: " + settingName);
}
return settings.get(settingName).getValue();
}
}
private Object get(String settingName) {
synchronized(LOCK) {
return settings.get(settingName).getValue();
}
}
public boolean getBoolean(String settingName) {
return (Boolean)get(settingName, Setting.BOOLEAN);
}
/**
* Gets a String setting.
*
* @param setting
* @return
*/
public String getString(String setting) {
return (String)get(setting, Setting.STRING);
}
public long getLong(String setting) {
return ((Number)(get(setting, Setting.LONG))).longValue();
}
/**
* Return a {@code HashMap} with a copy of the data of this setting.
*
* @param settingName The name of the setting
* @return The {@code HashMap} with the data of this setting.
* @throws SettingNotFoundException if a setting with this name doesn't
* exist or isn't a Map setting
*/
public Map getMap(String settingName) {
synchronized(LOCK) {
return new HashMap(getMapInternal(settingName));
}
}
/**
* Copies the current data of the Map of this setting into the given Map,
* then returns it for convenience.
*
* @param settingName The name of the setting
* @param map The map to put the data into
* @return The given Map, that now contains a copy of the data of this
* setting.
* @throws SettingNotFoundException if a setting with this name doesn't
* exist or isn't a Map setting
*/
public Map getMap(String settingName, Map map) {
synchronized (LOCK) {
map.putAll(getMapInternal(settingName));
return map;
}
}
/**
* Stores a copy of the data from <tt>map</tt> into the setting with the
* given name. Clears the setting Map first, so only the given data will be
* in there.
*
* @param settingName The name of the setting
* @param map The Map of data to put into the settings
* @throws SettingNotFoundException if a setting with this name doesn't
* exist or isn't a Map setting
*/
public boolean putMap(String settingName, Map map) {
synchronized (LOCK) {
Map settingMap = getMapInternal(settingName);
boolean changed = !settingMap.equals(map);
settingMap.clear();
settingMap.putAll(map);
return changed;
}
}
public Object mapGet(String settingName, Object key) {
synchronized(LOCK) {
return getMapInternal(settingName).get(key);
}
}
/**
* Puts a {@code key}-{@code value} pair into the {@code Map} with the name
* {@code settingName}.
*
* @param settingName The name of the setting
* @param key The key to put into the map
* @param value The value to put into the map
* @throws SettingNotFoundException if a setting with this name doesn't
* exist or isn't a Map setting
*/
public void mapPut(String settingName, Object key, Object value) {
synchronized(LOCK) {
getMapInternal(settingName).put(key, value);
}
}
/**
* Clears the Map of the setting with the given name.
*
* @param settingName
* @throws SettingNotFoundException if a setting with this name doesn't
* exist or isn't a Map setting
*/
public void mapClear(String settingName) {
synchronized(LOCK) {
getMapInternal(settingName).clear();
}
}
/**
* Removes {@code key} from the {@code Map} of the setting with
* {@code settingName}.
*
* @param settingName The name of the setting
* @param key The key to remove
* @throws SettingNotFoundException if a setting with this name doesn't
* exist or isn't a Map setting
*/
public void mapRemove(String settingName, Object key) {
synchronized (LOCK) {
getMapInternal(settingName).remove(key);
}
}
/**
* Returns an {@code ArrayList} containing a copy of the data in this
* setting.
*
* @param settingName The name of the setting
* @return The {@code ArrayList} containing a copy of the data
* @throws SettingNotFoundException if a setting with this name doesn't
* exist or isn't a List setting.
*/
public List getList(String settingName) {
synchronized (LOCK) {
return new ArrayList(getListInternal(settingName));
}
}
/**
* Stores a shallow copy of the data from the setting <tt>settingName</tt>
* into the given list.
*
* @param settingName
* @param list
* @throws SettingNotFoundException if a setting with this name doesn't
* exist or isn't a List setting.
*/
public void getList(String settingName, List list) {
synchronized (LOCK) {
list.addAll((Collection) get(settingName, Setting.LIST));
}
}
/**
* Saves a shallow copy of the given list into the settings. The given list
* should not be modified concurrently during this.
*
* @param settingName
* @param list
* @throws SettingNotFoundException if a setting with this name doesn't
* exist or isn't a List setting.
*/
public void putList(String settingName, List list) {
synchronized (LOCK) {
Collection settingList = (Collection) get(settingName, Setting.LIST);
settingList.clear();
settingList.addAll(list);
}
}
/**
* Checks if the List for the given setting contains the given {@code value}.
*
* @param settingName The name of the {@code List} setting
* @param value The value to check
* @return {@code true} if the value is contained in the {@code List}, {@code false} otherwise
* @throws SettingNotFoundException if a setting with this name doesn't
* exist or isn't a {@code List} setting.
*/
public boolean listContains(String settingName, Object value) {
synchronized(LOCK) {
return getListInternal(settingName).contains(value);
}
}
/**
* Removes {@code value} from the List setting with the name
* {@code settingName}.
*
* @param settingName The name of the setting
* @param value The value to remove
* @throws SettingNotFoundException if a setting with this name doesn't
* exist or isn't a List setting.
*/
public boolean listRemove(String settingName, Object value) {
synchronized(LOCK) {
return getListInternal(settingName).remove(value);
}
}
public void listAdd(String settingName, Object value) {
synchronized(LOCK) {
getListInternal(settingName).add(value);
}
}
public void listClear(String settingName) {
synchronized(LOCK) {
getListInternal(settingName).clear();
}
}
/**
* Adds the given <tt>Object</tt> to this <tt>List</tt> setting, if it
* wasn't already in there.
*
* @param settingName
* @param value
* @throws SettingNotFoundException if a setting with the given name doesn't
* exist or isn't a List-setting.
*/
public boolean setAdd(String settingName, Object value) {
synchronized(LOCK) {
Collection settingList = getListInternal(settingName);
if (!settingList.contains(value)) {
settingList.add(value);
return true;
}
return false;
}
}
/**
* Gets the Map of the setting with this name, that can be directly modified
* (if synchronized on LOCK).
*
* @param settingName The name of the setting
* @return The Map for this setting
*/
private Map getMapInternal(String settingName) {
synchronized (LOCK) {
return (Map) get(settingName, Setting.MAP);
}
}
/**
* Returns the actual List for this setting, that can directly be modified
* (if synchronized on <tt>LOCK</tt>).
*
* @param settingName
* @return
*/
private Collection getListInternal(String settingName) {
return (Collection) get(settingName, Setting.LIST);
}
/**
* Manually set a List or Map setting as changed, since those can't properly
* detect it themselves.
*
* @param settingName
*/
public void setSettingChanged(String settingName) {
if (isListSetting(settingName)) {
settingChanged(settingName, Setting.LIST, getList(settingName));
} else if (isMapSetting(settingName)) {
settingChanged(settingName, Setting.MAP, getMap(settingName));
}
}
public String settingValueToString(String setting) {
synchronized(LOCK) {
return settings.get(setting).toString();
}
}
public String settingToString(String setting) {
if (isBooleanSetting(setting)) {
return "Setting '"+setting+"' is "+getBoolean(setting)+".";
}
if (isLongSetting(setting)) {
return "Setting '"+setting+"' is "+getLong(setting)+".";
}
if (isStringSetting(setting)) {
return "Setting '"+setting+"' is '"+getString(setting)+"'.";
}
if (isMapSetting(setting)) {
return "Setting '"+setting+"' is '"+getMap(setting)+"'.";
}
if (isListSetting(setting)) {
return "Setting '"+setting+"' is '"+getList(setting)+"'.";
}
return null;
}
public String addTextual(String text) {
if (text == null || text.isEmpty()) {
return "Usage: /add <setting> <value>";
}
String[] split = text.trim().split(" ", 2);
if (split.length < 2) {
return "Usage: /add <setting> <value>";
}
String setting = split[0];
String parameter = split[1];
if (isListSetting(setting)) {
if (isOfSubtype(setting, Setting.STRING)) {
listAdd(setting, parameter);
} else if (isOfSubtype(setting, Setting.LONG)) {
try {
listAdd(setting, Long.parseLong(parameter));
} catch (NumberFormatException ex) {
return settingInvalidMessage(setting);
}
} else {
return settingInvalidMessage(setting);
}
return "Setting '"+setting+"' (List): Added '"+parameter+"', now: "+getList(setting);
}
return settingInvalidMessage(setting);
}
public String removeTextual(String text) {
if (text == null || text.isEmpty()) {
return "Usage: /remove <setting> <value>";
}
String[] split = text.trim().split(" ", 2);
if (split.length < 2) {
return "Usage: /remove <setting> <value>";
}
String setting = split[0];
String parameter = split[1];
if (isListSetting(setting)) {
if (isOfSubtype(setting, Setting.STRING)) {
listRemove(setting, parameter);
} else if (isOfSubtype(setting, Setting.LONG)) {
try {
listRemove(setting, Long.parseLong(parameter));
} catch (NumberFormatException ex) {
return settingInvalidMessage(setting);
}
} else {
return settingInvalidMessage(setting);
}
return "Setting '"+setting+"' (List): Removed '"+parameter+"', now: "+getList(setting);
}
return settingInvalidMessage(setting);
}
public String setTextual(String text) {
if (text == null || text.isEmpty()) {
return "Usage: /set <setting> <value>";
}
String[] split = text.split(" ", 2);
if (split.length < 2) {
return "Usage: /set <setting> <value>";
}
String setting = split[0];
String parameter = split[1];
if (isBooleanSetting(setting)) {
boolean value = false;
if (parameter.equals("1") || parameter.equals("true") ||
parameter.equals("on")) {
value = true;
}
if (parameter.equals("!")) {
value = !getBoolean(setting);
}
setBoolean(setting,value);
return "Setting '"+setting+"' set to "+value+".";
}
else if (isStringSetting(setting)) {
setString(setting,parameter);
return "Setting '"+setting+"' set to '"+parameter+"'.";
}
else if (isLongSetting(setting)) {
long value = 0;
try {
value = Long.parseLong(parameter);
} catch (NumberFormatException ex) {
return "Invalid value (must be numeric for this setting).";
}
setLong(setting,value);
return "Setting '"+setting+"' set to '"+parameter+"'.";
}
else if (isListSetting(setting) && isOfSubtype(setting, Setting.STRING)) {
listClear(setting);
listAdd(setting, parameter);
return "Setting '"+setting+"' (List) set to "+getList(setting);
}
else if (isMapSetting(setting) && isOfSubtype(setting, Setting.STRING)) {
String[] mapParameters = parameter.split(" ", 2);
if (mapParameters.length != 2) {
return "Invalid number of parameters to set map value.";
}
mapPut(setting, mapParameters[0], mapParameters[1]);
return "Setting '"+setting+"' (Map) set to "+getMap(setting);
}
return settingInvalidMessage(setting);
}
public String resetTextual(String text) {
if (text == null || text.isEmpty()) {
return "Usage: /reset <setting>";
}
String[] split = text.split(" ", 2);
String settingName = split[0];
if (getType(settingName) != Setting.UNDEFINED) {
int result = set(settingName, null, getType(settingName));
Object value = get(settingName);
if (result == Setting.CHANGED) {
return "Setting '"+settingName+"' reset to default ("+value.toString()+")";
} else {
return "Setting '"+settingName+"' already default ("+value.toString()+")";
}
}
return "Setting does not exist.";
}
public String getTextual(String text) {
if (text == null || text.isEmpty()) {
return "Usage: /get <setting>";
}
String[] split = text.split(" ");
String setting = split[0];
String output = settingToString(setting);
if (output != null) {
return output;
}
return "Setting does not exist.";
}
/**
* Sets the setting with the specified name to an empty value. This is only
* possible with Strings.
*
* @param text
* @return
*/
public String clearTextual(String text) {
if (text == null || text.isEmpty()) {
return "Usage: /clearSetting <setting>";
}
String[] split = text.split(" ");
String setting = split[0];
if (isBooleanSetting(setting)) {
return "Boolean settings can't be cleared.";
}
if (isLongSetting(setting)) {
return "Numeric settings can't be cleared.";
}
if (isStringSetting(setting)) {
setString(setting, "");
return "Setting '"+setting+"' set to empty string.";
}
return settingInvalidMessage(setting);
}
private String settingInvalidMessage(String setting) {
if (isSetting(setting)) {
return "Invalid setting: '"+setting+"' (can't change with this command).";
}
return "Invalid setting: '"+setting+"'.";
}
/**
* Turns all settings into a JSON String.
*
* @return The JSON
*/
private String settingsToJson(String file) {
JSONObject obj = new JSONObject();
Set<Map.Entry<String,Setting>> set = settings.entrySet();
for (Entry<String,Setting> entry : set) {
Setting setting = entry.getValue();
if (setting.allowedToSave() && setting.getFile().equals(file)) {
String key = entry.getKey();
Object value = setting.getValue();
// JSON Simple only supports List in this version
if (value instanceof Collection) {
value = new ArrayList((Collection)value);
}
obj.put(key, value);
}
}
return obj.toJSONString();
}
/**
* Parses the settings from a JSON String and adds them to the settings
* data. Only loads settings that were peviously defined and that can be
* saved.
*
* @param json
* @throws ParseException
*/
private void settingsFromJson(String json) throws ParseException {
JSONParser parser = new JSONParser();
JSONObject root = (JSONObject)parser.parse(json);
for (Entry<String,Setting> entry : settings.entrySet()) {
// For every defined setting
Setting setting = entry.getValue();
String settingName = entry.getKey();
Object obj = root.get(settingName);
if (setting.allowedToSave()) {
int objType = getTypeFromObject(obj);
//System.out.println(settingName+" "+objType);
if (objType == setting.getType()) {
//System.out.println("Loading setting: "+settingName+" "+obj);
if (objType == Setting.MAP) {
mapFromJson((Map)obj, (SubtypeSetting)setting);
}
else if (objType == Setting.LIST) {
listFromJson((List)obj, (SubtypeSetting)setting);
}
else {
setting.setValue(obj);
}
}
}
}
}
private void mapFromJson(Map map, SubtypeSetting setting) {
Map settingMap = (Map)setting.getValue();
for (Object key : map.keySet()) {
Object value = map.get(key);
if (getTypeFromObject(value) == setting.getSubType()) {
settingMap.put(key, value);
}
}
}
private void listFromJson(List list, SubtypeSetting setting) {
Collection settingList = (Collection)setting.getValue();
settingList.clear();
for (Object value : list) {
if (getTypeFromObject(value) == setting.getSubType()) {
settingList.add(value);
}
}
}
private int getTypeFromObject(Object obj) {
if (obj instanceof Number) {
return Setting.LONG;
}
else if (obj instanceof String) {
return Setting.STRING;
}
else if (obj instanceof Boolean) {
return Setting.BOOLEAN;
}
else if (obj instanceof Map) {
return Setting.MAP;
}
else if (obj instanceof List) {
return Setting.LIST;
}
return Setting.UNDEFINED;
}
/**
* Saves the settings to a file as JSON.
*
* @throws IOException
*/
public void saveSettingsToJson() {
aboutToSaveSettings();
synchronized(LOCK) {
System.out.println("Saving settings to JSON.");
saveSettingsToJson(defaultFile);
for (String fileName : files) {
saveSettingsToJson(fileName);
}
}
}
private void saveSettingsToJson(String fileName) {
LOGGER.info("Saving settings to file: "+fileName);
String json = settingsToJson(fileName);
Path file = Paths.get(fileName);
Path tempFile = Paths.get(fileName+"-temp");
try (BufferedWriter writer = Files.newBufferedWriter(tempFile, CHARSET)) {
writer.write(json);
MiscUtil.moveFile(tempFile, file);
} catch (IOException ex) {
LOGGER.warning("Error saving settings to file: "+ex);
System.out.println("Error saving settings to file: "+ex);
}
}
/**
* Loads the settings from a JSON file.
*/
public void loadSettingsFromJson() {
synchronized(LOCK) {
loadSettingsFromJson(defaultFile);
for (String fileName : files) {
loadSettingsFromJson(fileName);
}
}
}
private void loadSettingsFromJson(String fileName) {
LOGGER.info("Loading settings from file: "+fileName);
Path file = Paths.get(fileName);
try (BufferedReader reader = Files.newBufferedReader(file, CHARSET)) {
String input = reader.readLine();
if (input != null) {
settingsFromJson(input);
} else {
LOGGER.warning("Settings file empty: "+fileName);
LOGGER.log(Logging.USERINFO, "Settings file empty, using default settings ("+fileName+")");
}
} catch (IOException ex) {
LOGGER.warning("Error loading settings from file: "+ex);
} catch (ParseException ex) {
LOGGER.warning("Error parsing settings: "+ex);
LOGGER.log(Logging.USERINFO, "Settings file corrupt, using default settings ("+fileName+")");
}
}
public Collection<String> getSettingNames() {
synchronized(LOCK) {
return new HashSet<>(settings.keySet());
}
}
}