package com.superliminal.uiutil; import javax.swing.*; import java.util.*; import java.util.prefs.Preferences; import java.io.*; import java.awt.*; import java.net.*; /** * Title: PropertyManager * Description: Methods for getting property strings from cascading Properties objects. * The order of precidence is as follows: * <code> * top * system * userprefs * vendorprops * defaults * </code> * * The root of the chain is stored in the public static "top" member which should be * accessed for nearly all application purposes. Applications can set values there * as well which will take precedence over lower-level defaults but will not be * persisted across sessions. * * Property file names are assumed to be rooted in a folder named "resources" * expected to be found in the classpath. In that folder is expected to be * a property file named "defaults.prop". That file should contain default values * for all properties the application might request. This becomes the lowest level * Properties in a chain. * * An optional vendor-supplied property file may also be provided in which * vendors may specify overrides for custom versions of a published sub-set of * the default properties such as branding logos, colors, etc. A vendor property * file must be specified as a URL in an environment variable with the key * "vendorprops". e.g. vendorprops=http://newcorp.com/analyer/resources/vendor.prop. * the values specified in this file will take precidence over the analyzer defaults. * Note that images and other file resources referred to by that file must be * begin with the '/' character and will then be looked for relative to the * directory containing the vendor property file. * e.g. given the vendor prop file path above and the property setting: * main.logo.small=/newcorp.gif * will resolve to: * newcorp.com/analyer/resources/newcorp.gif * Also, quoted values will have their quotes stripped. This is in case a user * wants to create a path or other property that begins with '/'. * * Another special properties object in the chain is "userprefs" which is for end * user preferences which are stored in a file on the user's local machine in their home directory. * Properties that are set into this object are immediately persisted to the file * for retrieval in the current and future sessions. * * Copyright 2005, 2006 - Superliminal Software * @author Melinda Green */ public class PropertyManager extends Properties { private final static String PRODUCT_NAME = "App"; // wants to be passed in but would need to be environment variable. private final static String PREFS_ROOT_NAME = "com.superliminal.userprefs"; /** apps should load any user-specific property overrides directly into this object and call setProperty to customize. */ public static PropertyManager userprefs = new LocalProps(new File(getUserPrefFile())); private final static Properties sysprops = new PropertyManager(userprefs); /** * apps should typically only call getProperty on this object * although it is ok to store program arguments and other session overrides here too. */ public final static PropertyManager top = new PropertyManager(sysprops); static { init(); } // to perform static initialization /** users have no business subclassing this class so a private empty constructor will forbid it. */ private PropertyManager() {} public PropertyManager(Properties defaults) { super(defaults); } /** * Utility to load a properties object from a file. * @param prop_url points to a property file * @param into is the Properties object to populate. */ public static void loadProps(URL prop_url, Properties into) { if(prop_url == null) { //System.err.println("PropertyManager.loadProps: passed null url"); return; } try { URLConnection connection = prop_url.openConnection(); InputStream propstream = connection.getInputStream(); into.load(propstream); } catch (Exception e) { System.err.println("PropertyManager.init: Couldn't load property file '" + prop_url.getPath() + "'. Make sure this subpath can be found within the classpath."); } } /** * Utility to load a properties object from a file. * @param prop_file_name names a file expected to be found under ./resources * @param into is the Properties object to populate. */ public static void loadProps(String prop_file_name, Properties into) { String path = "resources" + File.separatorChar + prop_file_name; URL propurl = PropertyManager.class.getClassLoader().getResource(path); loadProps(propurl, into); } /** * Utility to load a set of command line arguments into a Properties object. * All argument names are expected to begin with a minus. If followed by an argument * without a minus, that argument is taken as the value for the one with the minus. * Arguments followed directly by another flagged argument are taken as boolean arguments * whos values are set to the string "true". * * @param args typically an args array from a main method * but with extracted elements possibly nulled out. * @param into the Properties file to load into. */ public static void loadProps(String args[], Properties into) { for (int i = 0; i < args.length; i++) { if(args[i] == null) continue; // skip any nulled out elements (caller propably used and extracted them) if (args[i].startsWith("-")) { // Make sure there's another arg if ((i + 1) < args.length) { // Make sure it's not another flag if(args[i+1] == null || args[i + 1].startsWith("-")) { // Must be a flag without a value, set to "true" into.setProperty(args[i].substring(1), "true"); } else { into.setProperty(args[i].substring(1), args[i + 1]); ++i; // skip to next arg pair } } else { // Must be a flag without a value at the end of the args into.setProperty(args[i].substring(1), "true"); } } else { // argument without a dash; must be malformed System.err.println("Invalid propertyfile argument: " + args[i]); } } } /** * A specialized PropertyManager that loads from a given local file. * Properties that are set on these objects are immediately persisted to that file * regardless of whether it existed originally. * This class is meant for handling user preferences that persist across sessions. */ private static class LocalProps extends PropertyManager { private File localPropFile; private boolean storeFailed = false; public LocalProps(File localPropFile) { this.localPropFile = localPropFile; if( ! localPropFile.exists()) return; // nothing to load try { load(new FileInputStream(localPropFile)); } catch (IOException e) { System.err.println("PropertyManager.LocalProps: Could not load local prop file '" + localPropFile.getAbsolutePath() + "'"); this.localPropFile = null; } } public void setFile(File newFile) { localPropFile.renameTo(newFile); localPropFile = newFile; storeFailed = false; writeToFile(); } /** * Calls super.setProperty() and then immediately attempts to store the entire contents to the user's preference file. */ public Object setProperty(String key, String value) { Object ret = super.setProperty(key, value); writeToFile(); return ret; } /** * Calls super.clear() and then immediately attempts to empty the user's preference file. */ public void clear() { super.clear(); writeToFile(); } /** * Calls super.remove() and then immediately attempts to store the entire contents to the user's preference file. */ public Object remove(Object key) { Object ret = super.remove(key); writeToFile(); return ret; } /** * Attempts to store the entire contents to the user's preference file. */ private void writeToFile() { if(localPropFile==null || storeFailed) return; try { FileOutputStream fos = new FileOutputStream(localPropFile); this.store(fos, PRODUCT_NAME + " User Preferences -- The location of this file can be changed via Edit->Preferences"); fos.close(); // important for renames to work } catch (IOException e) { storeFailed = true; // so as to only give fail msg once System.err.println("PropertyManager.LocalProps: Could not store local prop file '" + localPropFile.getAbsolutePath() + "'"); e.printStackTrace(); // crude exception handling but we can't count on logging or other services available. } } /** * Overrides to return keys alphabeticaly so that the store method will also write them in that order. * NOTE: The output of this method will always be sorted but for the sorted store operation to work * depends upon the Java implementation continuing to call this method when iterating. * That is likely but could possibly change. * @return alphabetically sorted key set. */ public Enumeration<Object> keys() { Vector keyList = new Vector(); for(Enumeration keysEnum = super.keys(); keysEnum.hasMoreElements(); ) keyList.add(keysEnum.nextElement()); Collections.sort(keyList); return keyList.elements(); } } // end class LocalProps /** * @return the path to where the user's preference file is or will be living. */ public static String getUserPrefFile() { //String userdefault = new File(javax.swing.filechooser.FileSystemView.getFileSystemView().getHomeDirectory(), PRODUCT_NAME+".props").getAbsolutePath(); File redsealdir = new File(System.getProperty("user.home") + File.separator + "redseal"); if( ! redsealdir.exists()) redsealdir.mkdir(); String userdefault = new File(redsealdir, PRODUCT_NAME+".props").getAbsolutePath(); return Preferences.userRoot().node(PREFS_ROOT_NAME).get(PRODUCT_NAME, userdefault); } /** * Sets the path for where user preferences will live. * @param newLoc is a file path to where the user preferences should now live. */ public static void setUserPrefFile(String newLoc) { ((LocalProps)userprefs).setFile(new File(newLoc)); Preferences.userRoot().node(PREFS_ROOT_NAME).put(PRODUCT_NAME, newLoc); } /** * High-level method that presents a file save dialog allowing the user to select a new location * for their user preferences file. * @param parent parent for the presented file chooser. */ public static void showPrefFileChooser(Component parent) { JFileChooser chooser = new JFileChooser(new File(getUserPrefFile())); //System.out.println("current file loc: " + getUserPrefFile()); if(chooser.showSaveDialog(parent) == JFileChooser.APPROVE_OPTION) { String newloc = chooser.getSelectedFile().getAbsolutePath(); //System.out.println("new location: " + newloc); setUserPrefFile(newloc); } } /** * A specialized PropertyManager that attempts to load a set of properties from a URL * representing a set of VAR, reseller, or customer-specific overrides for an * application's default property values. * See the ClientProp documentation above for descriptions of the subtle syntax * differences for file locations and quoted keys. */ private static class RemoteProps extends Properties { private String prefix; public RemoteProps(String fileurl, Properties def) { defaults = def; if(fileurl == null) return; URL url = null; try { url = new URL(fileurl); } catch(MalformedURLException e) { System.err.println("Couldn't open remote property file: " + fileurl); } if(url != null) loadProps(url, this); prefix = fileurl.substring(0, fileurl.lastIndexOf('/')); } /** * @return normal getProperty value except that for values beginning with '/', * prepends url path refering to same directory as the remote property file * given to the constructor. Also, quoted values will have their quotes striipped. * This is in case a user wants to create a path or other property beginning with '/'. */ public String getProperty(String key) { // for debugging vendor props, if uncommented, this will provide a good line // on which to set a breakpoint: //if(key.equalsIgnoreCase("main.logo.small")) // key = key; String val = (String) get(key); if(val == null) return defaults.getProperty(key); if(val.startsWith("/")) // path is relative to vendor prop file location val = prefix + val; if(val.startsWith("\"") && val.endsWith("\"")) // quoted strings for root paths or to contain spaces val = val.substring(1, val.length()-1); return val; } public String getProperty(String key, String dflt) { String val = getProperty(key); return val == null ? dflt : val; } } // end class RemoteProps /** * Static initializer to bootstrap the system. */ private static void init() { Properties sys = System.getProperties(); for(Enumeration e=sys.keys(); e.hasMoreElements(); ) { String key = (String)e.nextElement(); sysprops.setProperty(key, sys.getProperty(key)); //System.out.println(key + " = " + sys.getProperty(key)); } PropertyManager defs = new PropertyManager(); loadProps("defaults.prop", defs); // load any vendor-specific property file specified. // note, for testing, you can set a vm environment variable via something like: // -Dvendorprops=file:///C|/Superliminal/MC4D/resources/vendor.prop // or just hardcode with a line like this: // vendorpropfile = "file:///C|/Superliminal/MC4D/resources/vendor.prop"; String vendorpropfile = System.getProperty("vendorprops"); Properties vendorprops = new RemoteProps(vendorpropfile, defs); userprefs.defaults = new PropertyManager(vendorprops); } /** * Helper function to retrive integer values from the top properties. * @param key property name * @param def default value * @return integer value of top.getProperty(key) or default if not found or parsed. */ public static int getInt(String key, int def) { try { return Integer.parseInt(top.getProperty(key)); } catch(NumberFormatException nfe) {} return def; } /** * Helper function to retrive float values from the top properties. * @param key property name * @param def default value * @return float value of top.getProperty(key) or default if not found or parsed. */ public static float getFloat(String key, float def) { try { return Float.parseFloat(top.getProperty(key)); } catch(Exception e) {} return def; } /** * Helper function to retrive boolean values from the top properties. * @param key property name * @param def default value * @return boolean value of top.getProperty(key) or default if not found or parsed. */ public static boolean getBoolean(String key, boolean def) { try { String topval = top.getProperty(key); if(topval == null) return def; return Boolean.parseBoolean(topval); } catch(Exception e) {} return def; } /** * Helper function to retrive color objects from properties with the format * r,g,b where each channel is a value from 0 to 255. The color can also * be in hexadecimal format if it begins with "#", e.g., "#aabbcc" * @param key the color property name * @return a Color object with the parsed red, green, and blue values. */ public static Color getColor(String key, Color def) { String str = top.getProperty(key); if (str == null) return def; if (str.indexOf('#') >= 0) return Color.decode(str); StringTokenizer toc = new StringTokenizer(str, ","); int r = Integer.parseInt(toc.nextToken()); int g = Integer.parseInt(toc.nextToken()); int b = Integer.parseInt(toc.nextToken()); return new Color(r, g, b); } public static Color getColor(String key) { return getColor(key, null); } /** * Utility to construct a simple http url from keys to host and port properties. * @return an http url string. */ public static String getURL(String hostkey, String portkey) { return "http://" + top.getProperty(hostkey) + ":" + top.getProperty(portkey) + "/"; } /** * Simple example program. */ public static void main(String args[]) { // test of setting a top-level property, e.g. a setting specified on command line. PropertyManager.top.setProperty("debugging", "true"); // get a bunch of application values from the property files System.out.println("main.background = " + getColor("main.background")); System.out.println("tables.header.background = " + top.getProperty("tables.header.background") + " -> " + getColor("tables.header.background")); System.out.println("title = " + PropertyManager.top.getProperty("main.title")); // get a system property System.out.println("os.name = " + PropertyManager.top.getProperty("os.name")); // get a true top level (i.e. command line) property System.out.println("debugging = " + PropertyManager.top.getProperty("debugging")); int runcount = PropertyManager.getInt("test.runcount", 0); System.out.println("test.runcount = " + runcount); PropertyManager.userprefs.setProperty("test.runcount", ++runcount+""); System.out.println("test.runcount now = " + PropertyManager.userprefs.getProperty("test.runcount")); PropertyManager.userprefs.clear(); // testing that clearing userprefs writes the file System.out.println("num pref keys after clear: " + PropertyManager.userprefs.size()); PropertyManager.userprefs.setProperty("test.runcount", runcount+""); showPrefFileChooser(null); } }