package jaci.openrio.toast.lib.module; import com.grack.nanojson.JsonObject; import com.grack.nanojson.JsonParser; import com.grack.nanojson.JsonWriter; import jaci.openrio.toast.core.ToastBootstrap; import jaci.openrio.toast.core.io.usb.USBMassStorage; import jaci.openrio.toast.lib.profiler.Profiler; import jaci.openrio.toast.lib.profiler.ProfilerSection; import jaci.openrio.toast.lib.util.JSONUtil; import javax.script.ScriptException; import java.io.*; import java.lang.reflect.Array; import java.util.HashMap; import java.util.LinkedList; import java.util.Map; /** * The ModuleConfig is the new replacement for the old GroovyPreferences. * The ModuleConfig class allows for modules to take user-defined preferences regarding how the module functions. * The Configuration Files are stored under toast/config/ with the .conf file extension. These files are Formatted in JSON * and are automatically generated if the requested key does not exist. * * @author Jaci */ public class ModuleConfig { static File base_file; public static LinkedList<ModuleConfig> allConfigs; /** * Start the configuration engine. This sets up the Root dir as well as JavaScript bindings for Config.js. * This is executed by the Bootstrapper, no need to call it yourself. */ public static void init() { ProfilerSection section = Profiler.INSTANCE.section("JavaScript"); section.start("ModuleConfig"); base_file = new File(ToastBootstrap.toastHome, "config"); base_file.mkdirs(); allConfigs = new LinkedList<>(); section.stop("ModuleConfig"); } File parent_file; HashMap<String, Object> defaults; HashMap<String, Object> full_config; public ModuleConfig(String name) { try { if (USBMassStorage.config_highest != null) { base_file = new File(USBMassStorage.config_highest.toast_directory, "config"); base_file.mkdirs(); } if (!name.contains(".")) name = name + ".conf"; parent_file = new File(base_file, name); if (!parent_file.exists()) parent_file.createNewFile(); allConfigs.add(this); load(); } catch (Exception e) { } } public ModuleConfig(File file) { try { this.parent_file = file; if (!file.exists()) file.createNewFile(); allConfigs.add(this); load(); } catch (Exception e) { } } /** * Load a ModuleConfig. This method will flush the Bindings of the configuration file * and reload defaults. This should only be called by the constructor */ private void load() throws IOException, ScriptException { defaults = new HashMap<>(); try { reload(); } catch (Exception e) { e.printStackTrace(); } } /** * Reloads the configuration file. This will read the config file, add keys if they exist in the Defaults * Queue, as well as reformat the config file to PrettyPrint if needed. Call this if Config Files have been * changed (by the user) and you want to reload them. */ public void reload() { String json_config = ""; try { BufferedReader reader = new BufferedReader(new FileReader(parent_file)); String ln; while ((ln = reader.readLine()) != null) json_config += ln; reader.close(); } catch (Exception e) { } if (json_config.trim().length() == 0) json_config = "{}"; try { JsonObject obj = JsonParser.object().from(json_config); full_config = deepMerge(defaults, obj); PrintStream stream = new PrintStream(parent_file); String json = toJSON(); stream.println(json); stream.close(); } catch (Exception e) { e.printStackTrace(); } } /** * Unpack a period delimited string and get the object at that pointer. i.e test.test2.test3 translates to * test["test2"]["test3"] */ public static void unpack(HashMap<String, Object> object, String key, Object value) { String[] split = key.split("\\."); HashMap lobj = object; for (int cur = 0; cur < split.length; cur++) { String current = split[cur]; if (!(lobj.containsKey(current) && lobj.get(current) instanceof HashMap)) { lobj.put(current, new HashMap<String, Object>()); } if (cur == split.length - 1) { lobj.put(current, value); } else lobj = (HashMap) lobj.get(current); } } /** * Merge 2 Maps together deeply, merging inner maps and arrays. */ private static HashMap<String, Object> _deepMerge(HashMap<String, Object> defs, HashMap<String, Object> conf) { HashMap<String, Object> d = new HashMap<>(); HashMap<String, Object> c = new HashMap<>(); for (Map.Entry<String, Object> entry : defs.entrySet()) { unpack(d, entry.getKey(), entry.getValue()); } for (Map.Entry<String, Object> entry : conf.entrySet()) { unpack(c, entry.getKey(), entry.getValue()); } for (Map.Entry<String, Object> entryC : c.entrySet()) { String kC = entryC.getKey(); Object vC = entryC.getValue(); if (d.containsKey(kC)) { if (vC instanceof HashMap) { if (vC instanceof HashMap) { d.put(kC, _deepMerge((HashMap<String, Object>) d.get(kC), (HashMap<String, Object>) vC)); } else { d.put(kC, vC); } } else { d.put(kC, vC); } } else { d.put(kC, vC); } } return d; } /** * Deep merge a JSON object and the defaults array */ public static HashMap<String, Object> deepMerge(HashMap<String, Object> def, JsonObject conf) { return _deepMerge(def, JSONUtil.jsonToHash(conf)); } /** * Get an object from the configuration file. This will also post-process the data. Will return null if it * does not yet exist. */ public Object getObject(String name) { try { String[] keys = name.split("\\."); Object mir = full_config; for (String key : keys) { mir = ((HashMap) mir).get(key); } return mir; } catch (Exception e) { } return null; } /** * Convert the entire configuration into a JSON string. This does NOT post-process any information in the * configuration file, but will instead return the raw data as a JSONified string. This will also be pretty-printed */ public String toJSON() throws ScriptException { return JsonWriter.indent("\t").string().value(full_config).done(); } /** * The hash-code of the file of the configuration, preventing duplicates. */ @Override public int hashCode() { return parent_file.hashCode(); } /** * Checks whether a key exists in the configuration file. Alias to a null check on {@link #getObject(String)} */ public boolean has(String name) { return getObject(name) != null; } /** * Put a default key in the configuration. This is automatically called in most 'get' methods if the * key doesn't already exist in the configuration file. */ public void putDefault(String key, Object value) { defaults.put(key, value); reload(); } /** * Get a key from the config. If the key doesn't already exist, it will be created with the default value in the * configuration file. */ public Object getOrDefault(String name, Object def) { if (!has(name)) { putDefault(name, def); } Object get = getObject(name); return get == null ? def : get; //Just in case the File Write is locked for some reason } /** * Alias for {@link #getOrDefault(String, Object)}, coercing to a Number */ public Number getNumber(String name, Number def) { return (Number) getOrDefault(name, def); } /** * Alias for {@link #getOrDefault(String, Object)}, coercing to an Integer */ public int getInt(String name, int def) { return getNumber(name, def).intValue(); } /** * Alias for {@link #getOrDefault(String, Object)}, coercing to a Double */ public double getDouble(String name, double def) { return getNumber(name, def).doubleValue(); } /** * Alias for {@link #getOrDefault(String, Object)}, coercing to a Float */ public float getFloat(String name, float def) { return getNumber(name, def).floatValue(); } /** * Alias for {@link #getOrDefault(String, Object)}, coercing to a Long */ public long getLong(String name, long def) { return getNumber(name, def).longValue(); } /** * Alias for {@link #getOrDefault(String, Object)}, coercing to a Byte */ public byte getByte(String name, byte def) { return getNumber(name, def).byteValue(); } /** * Alias for {@link #getOrDefault(String, Object)}, coercing to a String */ public String getString(String name, String def) { return (String) getOrDefault(name, def); } /** * Alias for {@link #getOrDefault(String, Object)}, coercing to a Boolean */ public boolean getBoolean(String name, boolean def) { return (boolean) getOrDefault(name, def); } /** * Get an array from the Configuration File */ public <T> T[] getArray(String name, T[] def) { Object[] obj = ((Object[]) getOrDefault(name, def)); T[] objnew = (T[]) Array.newInstance(def.getClass().getComponentType(), obj.length); for (int i = 0; i < obj.length; i++) objnew[i] = (T) obj[i]; return objnew; } /** * Alias for {@link #getOrDefault(String, Object)} */ public Object get(String name, Object def) { return getOrDefault(name, def); } }