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);
}
}