Skip to content
Permalink
Browse files

Support grouping in preference files

Add a few groups to the preferences.ini file also, to take advantage of
this new feature. Groups are sorted alphabetically, and within groups,
items are sorted alphabetically, though within the group, another sort
order may be given to override this. This allows for more precise
sorting of values, and for larger preference files, makes them much
easier for users to read. Values without a group will be placed at the
top of the file.
  • Loading branch information...
LadyCailin committed Sep 25, 2019
1 parent d252b26 commit ae406b32ec4bfc04392df114b5b9eaa24ff4fbb1
@@ -5,6 +5,7 @@
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -25,8 +26,9 @@
*/
public class Preferences {

private final Map<String, Preference> prefs = new HashMap<String, Preference>();
private final Map<String, Map<String, Preference>> prefs = new HashMap<>();
private final String appName;
@SuppressWarnings("NonConstantLogger")
private final Logger logger;

private File prefFile;
@@ -78,10 +80,12 @@
/**
* The name of the preference
*/
@ObjectHelpers.StandardField
public String name;
/**
* The value of the preference, as a string
*/
@ObjectHelpers.ToString
public String value;
/**
* The allowed type of this value
@@ -97,24 +101,74 @@
*/
public Object objectValue;

/**
* The group name, by default the empty string, meaning don't group it.
*/
@ObjectHelpers.ToString
public String group = "";
/**
* The preference sort order, by default 100. Sorting takes place within groups, with preferences
* with identical sort values sorted alphabetically.
*/
public int sort = 100;

public Preference(String name, String def, Type allowed, String description) {
this.name = name;
this.value = def;
this.allowed = allowed;
this.description = description;
}

public Preference(String name, String def, Type allowed, String description, String group) {
this(name, def, allowed, description);
this.group = group;
}

public Preference(String name, String def, Type allowed, String description, int sort) {
this(name, def, allowed, description);
this.sort = sort;
}

public Preference(String name, String def, Type allowed, String description, String group, int sort) {
this(name, def, allowed, description, group);
this.sort = sort;
}

@Override
public String toString() {
return ObjectHelpers.DoToString(this);
}

@Override
@SuppressWarnings("EqualsWhichDoesntCheckParameterClass")
public boolean equals(Object o) {
return ObjectHelpers.DoEquals(this, o);
}

@Override
public int hashCode() {
return ObjectHelpers.DoHashCode(this);
}

}

/**
* Provide the name of the app, and logger, for recording errors, and a list of defaults, in case the value is not
* provided by the user, or an invalid value is provided. It also writes a custom header at the top of the file.
* Newlines are supported, but only \n
* @param appName
* @param logger
* @param defaults
* @param header
*/
public Preferences(String appName, Logger logger, List<Preference> defaults, String header) {
this.appName = appName;
this.logger = logger;
for(Preference p : defaults) {
prefs.put(p.name, p);
if(!prefs.containsKey(p.group)) {
prefs.put(p.group, new HashMap<>());
}
prefs.get(p.group).put(p.name, p);
}
if(!header.trim().isEmpty()) {
this.header = "# " + header.replaceAll("\n", "\n# ");
@@ -124,39 +178,63 @@ public Preferences(String appName, Logger logger, List<Preference> defaults, Str
/**
* Provide the name of the app, and logger, for recording errors, and a list of defaults, in case the value is not
* provided by the user, or an invalid value is provided.
* @param appName
* @param logger
* @param defaults
*/
public Preferences(String appName, Logger logger, List<Preference> defaults) {
this(appName, logger, defaults, "");
}

/**
* Searches through all preferences, regardless of group, and finds the Preference with the name.
* @param key
* @return
*/
private Preference getPrefFromKey(String key) {
for(Map<String, Preference> m : prefs.values()) {
if(m.containsKey(key)) {
return m.get(key);
}
}
return null;
}

/**
* Given a file that the preferences are supposedly stored in, this function will try to load the preferences. If
* the preferences don't exist, or they are incomplete, this will also fill in the missing values, and store the now
* complete preferences in the file location specified.
*
* @param prefFile
* @throws Exception
* @throws IOException
*/
public void init(File prefFile) throws IOException {
this.prefFile = prefFile;
if(prefFile != null && prefFile.exists()) {
Properties userProperties = new Properties();
FileInputStream in = new FileInputStream(prefFile);
userProperties.load(in);
in.close();
try(FileInputStream in = new FileInputStream(prefFile)) {
userProperties.load(in);
}
for(String key : userProperties.stringPropertyNames()) {
if(key.matches("\\[.*\\]")) {
// group name, skip it.
continue;
}
Preference p = getPrefFromKey(key);
String val = userProperties.getProperty(key);
String value = Objects.toString(getObject(val, prefs.get(key)), null);
Object ovalue = getObject(val, prefs.get(key));
Preference p1 = prefs.get(key);
String value = Objects.toString(getObject(val, p), null);
Object ovalue = getObject(val, p);
Preference p2;
if(p1 != null) {
p2 = new Preference(p1.name, value, p1.allowed, p1.description);
if(p != null) {
p2 = new Preference(p.name, value, p.allowed, p.description, p.group, p.sort);
} else {
p2 = new Preference(key, val, Type.STRING, "");
}
p2.objectValue = ovalue;
prefs.put(key, p2);
if(!prefs.containsKey(p2.group)) {
prefs.put(p2.group, new HashMap<>());
}
prefs.get(p2.group).put(key, p2);
}
}
save();
@@ -239,13 +317,14 @@ private Boolean getBoolean(String value) {
}

private Object getSafePreference(String name, Type type) {
if(prefs.get(name).allowed != type) {
throw new IllegalArgumentException("Expecting " + prefs.get(name).allowed + " but " + type + " was requested");
Preference p = getPrefFromKey(name);
if(p.allowed != type) {
throw new IllegalArgumentException("Expecting " + p.allowed + " but " + type + " was requested");
}
if(prefs.get(name).objectValue == null) {
prefs.get(name).objectValue = getObject(prefs.get(name).value, prefs.get(name));
if(p.objectValue == null) {
p.objectValue = getObject(p.value, p);
}
return prefs.get(name).objectValue;
return p.objectValue;
}

/**
@@ -314,6 +393,7 @@ public String getStringPreference(String name) {
return (String) getSafePreference(name, Type.STRING);
}

@SuppressWarnings("UseSpecificCatch")
private void save() {
try {
StringBuilder b = new StringBuilder();
@@ -328,10 +408,26 @@ private void save() {
if(!header.trim().isEmpty()) {
b.append(header).append(nl).append(nl);
}
SortedSet<String> keys = new TreeSet<String>(prefs.keySet()) {
};
for(String key : keys) {
Preference p = prefs.get(key);
SortedSet<Preference> prfs = new TreeSet<>((Preference t, Preference t1) -> {
int groupSort = t.group.compareTo(t1.group);
if(groupSort != 0) {
return groupSort;
}
if(t.sort == t1.sort) {
return t.name.compareTo(t1.name);
}
return t.sort < t1.sort ? -1 : 1;
});
for(Map<String, Preference> m : prefs.values()) {
prfs.addAll(m.values());
}
String currentGroup = "";
for(Preference p : prfs) {
if(!p.group.equals(currentGroup)) {
b.append("[").append(p.group).append("]\n");
currentGroup = p.group;
}
// Preference p = getPrefFromKey(key);
String description = "This value is not used in " + appName;
if(!p.description.trim().isEmpty()) {
description = p.description;
@@ -100,6 +100,10 @@ public String config() {
}
}

private static final String LOGGING_GROUP = "Logging";
private static final String PROFILING_GROUP = "Profiling";
private static final String SECURITY_GROUP = "Security";
private static final String DEBUG_GROUP = "Debugging";
/**
* Initializes the global Prefs to this file.
*
@@ -109,18 +113,18 @@ public String config() {
public static void init(final File f) throws IOException {
ArrayList<Preferences.Preference> a = new ArrayList<>();
a.add(new Preference(PNames.DEBUG_MODE.config(), "false", Preferences.Type.BOOLEAN, "Whether or not to display"
+ " debug information in the console"));
+ " debug information in the console", DEBUG_GROUP));
a.add(new Preference(PNames.SHOW_WARNINGS.config(), "true", Preferences.Type.BOOLEAN, "Whether or not to display"
+ " warnings in the console, while compiling"));
a.add(new Preference(PNames.CONSOLE_LOG_COMMANDS.config(), "true", Preferences.Type.BOOLEAN, "Whether or not to"
+ " display the original command in the console when it is run"));
+ " display the original command in the console when it is run", LOGGING_GROUP));
a.add(new Preference(PNames.SCRIPT_NAME.config(), "aliases.msa", Preferences.Type.STRING, "The path to the"
+ " default config file, relative to the CommandHelper plugin folder"));
a.add(new Preference(PNames.ENABLE_INTERPRETER.config(), "false", Preferences.Type.BOOLEAN, "Whether or not to"
+ " enable the /interpreter command. Note that even with this enabled, a player must still have the"
+ " commandhelper.interpreter permission, but"
+ " setting it to false prevents all players from accessing the interpreter regardless of their"
+ " permissions."));
+ " permissions.", SECURITY_GROUP));
a.add(new Preference(PNames.BASE_DIR.config(), "", Preferences.Type.STRING, "The base directory/directories"
+ " that scripts"
+ " can read and write to. If left blank, then the default of the server directory will be used. "
@@ -131,7 +135,7 @@ public static void init(final File f) throws IOException {
+ " as accessible, even if the symlink itself is within another entry in the list. Note that empty"
+ " paths are supported when splitting the path, and having a trailing ';' will cause the default"
+ " path to be added, so don't end the path with a trailing ; if you don't intend for the path to"
+ " include the default."));
+ " include the default.", SECURITY_GROUP));
a.add(new Preference(PNames.PLAY_DIRTY.config(), "false", Preferences.Type.BOOLEAN, "Makes CommandHelper play"
+ " dirty and break all sorts of programming rules, so that other plugins can't interfere with the"
+ " operations that you defined. Note that doing this essentially makes CommandHelper have absolute"
@@ -143,27 +147,27 @@ public static void init(final File f) throws IOException {
a.add(new Preference(PNames.MAIN_FILE.config(), "main.ms", Preferences.Type.STRING, "The path to the main file,"
+ " relative to the CommandHelper folder"));
a.add(new Preference(PNames.ALLOW_DEBUG_LOGGING.config(), "false", Preferences.Type.BOOLEAN, "If set to false,"
+ " the Debug class of functions will do nothing."));
+ " the Debug class of functions will do nothing.", LOGGING_GROUP));
a.add(new Preference(PNames.DEBUG_LOG_FILE.config(), "logs/debug/%Y-%M-%D-debug.log", Preferences.Type.STRING,
"The path to the debug output log file. Six variables are available, %Y, %M, and %D, %h, %m, %s, which"
+ " are replaced with the current year, month, day, hour, minute and second respectively. It is"
+ " highly recommended that you use at least year, month, and day if you are for whatever"
+ " reason leaving logging on, otherwise the file size would get excessively large. The path"
+ " is relative to the CommandHelper directory and is not bound by the base-dir restriction."
+ " The logger preferences file is created in the same directory this file is in as well, and"
+ " is named loggerPreferences.txt"));
+ " is named loggerPreferences.txt", LOGGING_GROUP));
a.add(new Preference(PNames.STANDARD_LOG_FILE.config(), "logs/%Y-%M-%D-commandhelper.log",
Preferences.Type.STRING, "The path the standard log files that the log() function writes to. Six"
+ " variables are available, %Y, %M, and %D, %h, %m, %s, which are replaced with the current"
+ " year, month, day, hour, minute and second respectively. It is highly recommended that you"
+ " use at least year, month, and day if you are actively logging things, otherwise the file"
+ " size would get excessively large. The path is relative to the CommandHelper directory and"
+ " is not bound by the base-dir restriction."));
+ " is not bound by the base-dir restriction.", LOGGING_GROUP));
a.add(new Preference(PNames.ALLOW_PROFILING.config(), "false", Preferences.Type.BOOLEAN, "If set to false, the"
+ " Profiling class of functions will do nothing."));
+ " Profiling class of functions will do nothing.", PROFILING_GROUP));
a.add(new Preference(PNames.PROFILING_FILE.config(), "logs/profiling/%Y-%M-%D-profiling.log",
Preferences.Type.STRING, "The path to the profiling logs. These logs are perf4j formatted logs. Consult"
+ " the documentation for more information."));
+ " the documentation for more information.", PROFILING_GROUP));
a.add(new Preference(PNames.SHOW_SPLASH_SCREEN.config(), "true", Preferences.Type.BOOLEAN, "Whether or not to"
+ " show the splash screen at server startup"));
a.add(new Preference(PNames.USE_COLORS.config(),
@@ -173,31 +177,31 @@ public static void init(final File f) throws IOException {
+ " halt compilation of pure mscript files if a compilation failure occurs in any one of the files."));
a.add(new Preference(PNames.USE_SUDO_FALLBACK.config(), "false", Preferences.Type.BOOLEAN, "If true, sudo()"
+ " will use a less safe fallback method if it fails. See the documentation on the sudo function for"
+ " more details. If this is true, a warning is issued at startup."));
+ " more details. If this is true, a warning is issued at startup.", SECURITY_GROUP));
a.add(new Preference(PNames.ALLOW_SHELL_COMMANDS.config(), "false", Preferences.Type.BOOLEAN, "If true, allows"
+ " for the shell functions to be used from outside of cmdline mode. WARNING: Enabling these functions"
+ " can be extremely dangerous if you accidentally allow uncontrolled access to them, and can"
+ " grant full control of your server if not careful. Leave this set to false unless you really know"
+ " what you're doing."));
+ " what you're doing.", SECURITY_GROUP));
a.add(new Preference(PNames.ALLOW_DYNAMIC_SHELL.config(), "false", Preferences.Type.BOOLEAN, "If true, allows"
+ " use of the shell() functions from dynamic code sources, i.e"
+ " interpreter or eval(). This almost certainly should always remain false, and if enabled, enabled"
+ " only temporarily. If this is true, if an account with"
+ " interpreter mode is compromised, the attacker could gain access to your entire server, under the"
+ " user running minecraft, not just the game server."));
+ " user running minecraft, not just the game server.", SECURITY_GROUP));
a.add(new Preference(PNames.SCREAM_ERRORS.config(), "false", Preferences.Type.BOOLEAN, "Setting this to true"
+ " allows you to scream errors. Regardless of other settings"
+ " that you may have unintentionally configured, this will override all ways of suppressing fatal"
+ " errors, including uncaught exception"
+ " handlers, error logging turned off, etc. This is meant as a last ditch effort to diagnosing an"
+ " error. This implicitely turns debug mode"
+ " on as well, which will cause even more error logging to occur."));
+ " on as well, which will cause even more error logging to occur.", DEBUG_GROUP));
a.add(new Preference(PNames.INTERPRETER_TIMEOUT.config(), "15", Preferences.Type.INT, "Sets the time (in"
+ " minutes) that interpreter mode is unlocked for when /interpreter-on is run from console. Set to 0"
+ " (or a negative number)"
+ " to disable this feature, and allow interpreter mode all the time. It is highly recommended that"
+ " you leave this set to some number greater than 0, to enhance"
+ " server security, and require a \"two step\" authentication for interpreter mode."));
+ " server security, and require a \"two step\" authentication for interpreter mode.", SECURITY_GROUP));
a.add(new Preference(PNames.STRICT_MODE.config(), "false", Preferences.Type.BOOLEAN, "If set to true, forces"
+ " all files that do not specifically set strict mode on or off into strict mode. See the"
+ " documentation for more information about what strict mode does."));

0 comments on commit ae406b3

Please sign in to comment.
You can’t perform that action at this time.