package com.laytonsmith.PureUtilities; import com.laytonsmith.PureUtilities.Common.FileUtil; import com.laytonsmith.PureUtilities.Common.StringUtils; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.SortedSet; import java.util.TreeSet; import java.util.logging.Level; import java.util.logging.Logger; /** * This class allows an application to more easily manage user preferences. As an application grows, more preferences * will likely be added, but if the application uses flat file storage, managing these preferences while adding new * preferences can be difficult. This class manages, documents, and provides default values, all the while not * interfering with changes the user has made, meaning that you are free to add new preferences, or change default * values, without fear of changing values that the user has specifically set. For sample usage, see * https://gist.github.com/1042094 */ public class Preferences { private final Map<String, Preference> prefs = new HashMap<String, Preference>(); private final String appName; private final Logger logger; private File prefFile; private String header = ""; private int lineLength = 120; /** * The type a particular preference can be. The value will be cast to the given type if possible. NUMBER and DOUBLE * are guaranteed to be castable to a Double. NUMBER can also sometimes be cast to an int. BOOLEAN is cast to a * boolean, and may be stored in the preferences file as either true/false, yes/no, on/off, or a number, which get * parsed accordingly. STRING can be any value. */ public enum Type { NUMBER, BOOLEAN, STRING, INT, DOUBLE } /** * An object corresponding to a single preference */ public static class Preference { /** * The name of the preference */ public String name; /** * The value of the preference, as a string */ public String value; /** * The allowed type of this value */ public Type allowed; /** * The description of this preference. Used to write out to file. */ public String description; /** * The object representation of this value. Should not be used directly. */ public Object objectValue; public Preference(String name, String def, Type allowed, String description) { this.name = name; this.value = def; this.allowed = allowed; this.description = description; } } /** * 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 */ 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 (!header.trim().isEmpty()) { this.header = "# " + header.replaceAll("\n", "\n# "); } } /** * 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. */ public Preferences(String appName, Logger logger, List<Preference> defaults) { this(appName, logger, defaults, ""); } /** * 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 */ 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(); for (String key : userProperties.stringPropertyNames()) { String val = userProperties.getProperty(key); String value = getObject(val, ((Preference) prefs.get(key))).toString(); Object ovalue = getObject(val, ((Preference) prefs.get(key))); Preference p1 = prefs.get(key); Preference p2; if (p1 != null) { p2 = new Preference(p1.name, value, p1.allowed, p1.description); } else { p2 = new Preference(key, val, Type.STRING, ""); } p2.objectValue = ovalue; prefs.put(key, p2); } } save(); } private Object getObject(String value, Preference p) { if (p == null) { return value; } if (value.equalsIgnoreCase("null")) { return getObject(p.value, p); } switch (p.allowed) { case INT: try { return Integer.parseInt(value); } catch (NumberFormatException e) { logger.log(Level.WARNING, "[" + appName + "] expects the value of " + p.name + " to be an integer. Using the default of " + p.value); return Integer.parseInt(p.value); } case DOUBLE: try { return Double.parseDouble(value); } catch (NumberFormatException e) { logger.log(Level.WARNING, "[" + appName + "] expects the value of " + p.name + " to be an double. Using the default of " + p.value); return Double.parseDouble(p.value); } case BOOLEAN: try { return getBoolean(value); } catch (NumberFormatException e) { logger.log(Level.WARNING, "[" + appName + "] expects the value of " + p.name + " to be an boolean. Using the default of " + p.value); return getBoolean(p.value); } case NUMBER: try { return Integer.parseInt(value); } catch (NumberFormatException e) { try { return Double.parseDouble(value); } catch (NumberFormatException f) { logger.log(Level.WARNING, "[" + appName + "] expects the value of " + p.name + " to be a number. Using the default of " + p.value); try { return Integer.parseInt(p.value); } catch (NumberFormatException g) { return Double.parseDouble(p.value); } } } case STRING: default: return value; } } private Boolean getBoolean(String value) { if (value.equalsIgnoreCase("true")) { return true; } else if (value.equalsIgnoreCase("false")) { return false; } else if (value.equalsIgnoreCase("yes")) { return true; } else if (value.equalsIgnoreCase("no")) { return false; } else if (value.equalsIgnoreCase("on")) { return true; } else if (value.equalsIgnoreCase("off")) { return false; } else { double d = Double.parseDouble(value); if (d == 0) { return false; } else { return true; } } } /** * Returns the value of a preference, cast to the appropriate type. * * @param name * @return */ public Object getPreference(String name) { if (prefs.get(name).objectValue == null) { prefs.get(name).objectValue = getObject(prefs.get(name).value, prefs.get(name)); } return prefs.get(name).objectValue; } private void save() { try { StringBuilder b = new StringBuilder(); String nl = System.getProperty("line.separator"); b.append("# This file is generated automatically. Changes made to the values of this file") .append(nl) .append("# will persist, but changes to comments will not.") .append(nl).append(nl); 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); String description = "This value is not used in " + appName; if (!p.description.trim().isEmpty()) { description = p.description; } StringBuilder c = new StringBuilder(); boolean first = true; for (String line : description.split("\n|\r\n|\n\r")) { for (String line2 : StringUtils.lineSplit(line, lineLength)) { if (first) { c.append("# ").append(line2); first = false; } else { c.append(nl).append("# ").append(line2); } } } b.append(c).append(nl).append(p.name).append("=").append(p.value).append(nl).append(nl); } if (prefFile != null && !prefFile.exists()) { prefFile.getAbsoluteFile().getParentFile().mkdirs(); prefFile.createNewFile(); } if (prefFile != null) { FileUtil.write(b.toString(), prefFile); } } catch (Exception ex) { logger.log(Level.WARNING, "[" + appName + "] Could not write out preferences file: " + (prefFile != null ? prefFile.getAbsolutePath() : "null"), ex); } } /** * Sets the comment line length. * * @param lineLength The length, an integer greater than 0. * @throws IllegalArgumentException If {@code lineLength} is less than 1. */ public void setLineLength(int lineLength) { if (lineLength < 1) { throw new IllegalArgumentException(); } this.lineLength = lineLength; } }