diff --git a/projects/samskivert/src/java/com/samskivert/util/ConfigUtil.java b/projects/samskivert/src/java/com/samskivert/util/ConfigUtil.java index 7966e8cb..8249cad7 100644 --- a/projects/samskivert/src/java/com/samskivert/util/ConfigUtil.java +++ b/projects/samskivert/src/java/com/samskivert/util/ConfigUtil.java @@ -1,5 +1,5 @@ // -// $Id: ConfigUtil.java,v 1.7 2002/10/16 01:52:37 mdb Exp $ +// $Id: ConfigUtil.java,v 1.8 2002/11/25 22:25:20 mdb Exp $ // // samskivert library - useful routines for java programs // Copyright (C) 2001 Michael Bayne @@ -34,9 +34,7 @@ public class ConfigUtil { /** * Loads a properties file from the named file that exists somewhere - * in the classpath. A full path should be supplied, but variations - * including and not including a leading slash will be used because - * JVMs differ on their opinion of whether this is necessary. + * in the classpath. * *

The classloader that loaded the ConfigUtil class * is searched first, followed by the system classpath. If you wish to @@ -45,7 +43,7 @@ public class ConfigUtil * * @param path The path to the properties file, relative to the root * of the classpath entry from which it will be loaded - * (e.g. /conf/foo.properties). + * (e.g. com/foo/config.properties). * * @return A properties object loaded with the contents of the * specified file if the file could be found, null otherwise. @@ -57,59 +55,163 @@ public class ConfigUtil } /** - * Like the other version of {@link #loadProperties(String)} - * but this one uses the supplied class loader rather than the class - * loader used to load the ConfigUtil class. + * Like {@link #loadProperties(String)} but this method uses the + * supplied class loader rather than the class loader used to load the + * ConfigUtil class. * * @see #loadProperties(String) */ public static Properties loadProperties (String path, ClassLoader loader) throws IOException { - InputStream in = getStream(path, loader); - if (in == null) { - throw new FileNotFoundException(path); - } - - Properties props = null; - props = new Properties(); - props.load(in); - + Properties props = new Properties(); + loadProperties(path, loader, props); return props; } + /** + * Like {@link #loadProperties(String,ClassLoader)} but the properties + * are loaded into the supplied {@link Properties} object. + */ + public static void loadProperties ( + String path, ClassLoader loader, Properties target) + throws IOException + { + InputStream in = getStream(path, loader); + if (in == null) { + throw new FileNotFoundException(path); + } + target.load(in); + in.close(); + } + /** * Creates a properties instance by combining properties files loaded - * using the specified classpath-relative property file path. A - * leading slash should be supplied, but variations including and not - * including a leading slash will be used because JVMs differ on their - * opinion of whether this is necessary. + * using the specified classpath-relative property file + * path. * - *

The inheritance works like so: the file will be searched for in - * the classpath from farthest to nearest. Near and far in the - * classpath are defined by the class loading search order. Normal - * class loading searches from nearest to farthest. Beginning with the - * farthest copy of the properties file, sucessively nearer copies - * will be overlaid onto those properties to achieve a sort of - * inheritance. Properties specified in nearer versions of the file - * will override those in farther versions of the file, but properties - * not specified in nearer versions will be "inherited" from the - * farther versions. Using this mechanism, a standard set of defaults - * can be provided and users need only place a properties file with - * their preferred overrides somewhere nearer in the classpath to have - * those overrides properly combined with the original - * defaults. Because the entire classpath is searched, this process - * can cascade up through a set of properties files and provide a - * powerful mechanism for inheriting configuration information. + *

The inheritance works in two ways: * - *

The classloader that loaded the ConfigUtil class - * is searched first, followed by the system classpath. If you wish to - * provide an additional classloader, use the version of this function - * that takes a classloader as an argument. + *

+ * + * A final note: All of the inheritance directives must be + * grouped together in an uninterrupted sequence of lines. One the + * parsing code finds the first directive, it stops parsing when it + * sees a line that does not contain a directive. * * @param path The path to the properties file, relative to the root * of the classpath entries from which it will be loaded - * (e.g. /conf/foo.properties). + * (e.g. com/foo/config.properties). * * @return A properties object loaded with the contents of the * specified file if the file could be found, null otherwise. @@ -122,15 +224,30 @@ public class ConfigUtil } /** - * Like the other version of {@link #loadInheritedProperties(String)} - * but this one uses the supplied class loader rather than the class - * loader used to load the ConfigUtil class. + * Like {@link #loadInheritedProperties(String)} but this method uses + * the supplied class loader rather than the class loader used to load + * the ConfigUtil class. * * @see #loadInheritedProperties(String) */ public static Properties loadInheritedProperties ( String path, ClassLoader loader) throws IOException + { + Properties props = new Properties(); + loadInheritedProperties(path, loader, props); + return props; + } + + /** + * Like {@link #loadInheritedProperties(String,ClassLoader)} but the + * properties are loaded into the supplied properties object. + * Properties that already exist in the supplied object will be + * overwritten by the loaded properties where they have the same key. + */ + public static void loadInheritedProperties ( + String path, ClassLoader loader, Properties target) + throws IOException { // first look for the files in the supplied class loader Enumeration enum = getResources(path, loader); @@ -144,8 +261,7 @@ public class ConfigUtil } } - // we need to process the resources in reverse order, so we put - // them all into an array first + // stick the matches into an array list so that we can count them ArrayList rsrcs = new ArrayList(); while (enum.hasMoreElements()) { rsrcs.add(enum.nextElement()); @@ -156,36 +272,223 @@ public class ConfigUtil // works around a bug in Tomcat 3.3's classloader wherein it // simply returns nothing to all calls to getResources() if (rsrcs.size() == 0) { - // if getResourceAsStream() returns something, but - // getResources() returned nothing, then we know there's a bug - // and we do our best to work around it - if (getResourceAsStream(path, loader) != null) { + // if getStream() returns something, but getResources() + // returned nothing, then we know there's a bug and we do our + // best to work around it + InputStream in = getStream(path, loader); + if (in != null) { Log.warning("Buggy classloader: getResources() returned " + "no resources, but getResourceAsStream() " + "returned a resource [path=" + path + - ", loader=" + loader.getClass().getName() + - "]. Returning the one we could get our hands on."); - return loadProperties(path, loader); + ", loader=" + StringUtil.shortClassName(loader) + + "]. Using the one we could get our hands on."); + target.load(in); + in.close(); + } + throw new FileNotFoundException(path); + } + + // load up the metadata for the properties files + PropRecord root = null, crown = null; + HashMap map = null; + + if (rsrcs.size() == 1) { + // parse the metadata for our only properties file + root = parseMetaData(path, (URL)rsrcs.get(0)); + + } else { + map = new HashMap(); + for (int ii = 0; ii < rsrcs.size(); ii++) { + // parse the metadata for this properties file + PropRecord record = parseMetaData(path, (URL)rsrcs.get(ii)); + + // make sure the record we parseded is valid because we're + // going to be doing overrides + record.validate(); + + // then map it according to its package defintion + map.put(record._package, record); + + // if this guy overrides nothing, he's the root property + if (record._overrides == null) { + // make sure there aren't two or more roots + if (root != null) { + String errmsg = record + " cannot have the same " + + "path as " + root + " without one overriding " + + "the other."; + throw new IOException(errmsg); + } + root = record; + } + } + + // now wire up all the records according to the hierarchy + Iterator iter = map.values().iterator(); + while (iter.hasNext()) { + PropRecord prec = (PropRecord)iter.next(); + if (prec._overrides == null) { + // sanity check + if (prec != root) { + String errmsg = "Internal error: found multiple " + + "roots where we shouldn't have [root=" + root + + ", prec=" + prec + "]"; + throw new IOException(errmsg); + } + continue; + } + + // wire this guy up to whomever he overrides + for (int ii = 0; ii < prec._overrides.length; ii++) { + String ppkg = prec._overrides[ii]; + PropRecord parent = (PropRecord)map.get(ppkg); + if (parent == null) { + throw new IOException("Cannot find parent '" + ppkg + + "' for " + prec); + } + parent.add(prec); + } + } + + // verify that there is only one crown + ArrayList crowns = new ArrayList(); + iter = map.values().iterator(); + while (iter.hasNext()) { + PropRecord prec = (PropRecord)iter.next(); + if (prec.size() == 0) { + crowns.add(prec); + } + } + + if (crowns.size() == 0) { + String errmsg = "No top-level property override could be " + + "found, perhaps there are circular references " + + StringUtil.toString(map.values()); + throw new IOException(errmsg); + + } else if (crowns.size() > 1) { + String errmsg = "Multiple top-level properties were found, " + + "one definitive top-level file must provide an order " + + "for all others " + StringUtil.toString(crowns); + throw new IOException(errmsg); + } + + crown = (PropRecord)crowns.get(0); + } + + // if the root extends another file, resolve that first + if (!StringUtil.blank(root._extends)) { + try { + loadInheritedProperties(root._extends, loader, target); + } catch (FileNotFoundException fnfe) { + String errmsg = "Unable to locate parent '" + root._extends + + "' for '" + root.path + "'"; + throw new IOException(errmsg); } } - // now load each file in turn into our properties object - Properties props = new Properties(); - for (int i = rsrcs.size()-1; i >= 0; i--) { - URL rurl = (URL)rsrcs.get(i); - InputStream in = rurl.openStream(); - props.load(in); + // now apply all of the overrides, in the appropriate order + if (crown != null) { + HashMap applied = new HashMap(); + loadPropertiesOverrides(crown, map, applied, loader, target); + + } else { + // we have no overrides, so load our props up straight + InputStream in = root.sourceURL.openStream(); + target.load(in); + in.close(); } - return props; + // finally remove the metadata properties + target.remove(PACKAGE_KEY); + target.remove(EXTENDS_KEY); + target.remove(OVERRIDES_KEY); + } + + /** {@link #loadInheritedProperties(String,ClassLoader,Properties)} + * helper function. */ + protected static void loadPropertiesOverrides ( + PropRecord prec, HashMap records, HashMap applied, + ClassLoader loader, Properties target) + throws IOException + { + if (applied.containsKey(prec._package)) { + return; + } + + // first load up properties from all of our parent nodes + if (prec._overrides != null) { + for (int ii = 0; ii < prec._overrides.length; ii++) { + PropRecord parent = (PropRecord) + records.get(prec._overrides[ii]); + loadPropertiesOverrides( + parent, records, applied, loader, target); + } + } + + // now apply our properties values + InputStream in = prec.sourceURL.openStream(); + target.load(in); + in.close(); + applied.put(prec._package, prec); + } + + /** + * Performs simple processing of the supplied input stream to obtain + * inheritance metadata from the properties file. + */ + protected static PropRecord parseMetaData (String path, URL sourceURL) + throws IOException + { + InputStream input = sourceURL.openStream(); + BufferedReader bin = new BufferedReader(new InputStreamReader(input)); + PropRecord record = new PropRecord(path, sourceURL); + boolean started = false; + String line; + + while ((line = bin.readLine()) != null) { + if (line.startsWith(PACKAGE_KEY)) { + record._package = parseValue(line); + started = true; + } else if (line.startsWith(EXTENDS_KEY)) { + record._extends = parseValue(line); + started = true; + } else if (line.startsWith(OVERRIDES_KEY)) { + record._overrides = parseValues(line); + started = true; + } else if (started) { + break; + } + } + input.close(); + + return record; + } + + /** {@link #parseMetaData} helper function. */ + protected static String parseValue (String line) + { + int eidx = line.indexOf("="); + return (eidx == -1) ? "" : line.substring(eidx+1).trim(); + } + + /** {@link #parseMetaData} helper function. */ + protected static String[] parseValues (String line) + { + String value = parseValue(line); + if (StringUtil.blank(value)) { + return null; + } + + String[] values = StringUtil.split(value, ","); + for (int ii = 0; ii < values.length; ii++) { + values[ii] = values[ii].trim(); + } + return values; } /** * Returns an input stream referencing a file that exists somewhere in - * the classpath. A full path (relative to the classpath directories) - * should be supplied, but variations including and not including a - * leading slash will be used because JVMs differ on their opinion of - * whether this is necessary. + * the classpath. * *

The classloader that loaded the ConfigUtil class * is searched first, followed by the system classpath. If you wish to @@ -194,8 +497,7 @@ public class ConfigUtil * * @param path The path to the file, relative to the root of the * classpath directory from which it will be loaded - * (e.g. /conf/foo.gif or perhaps just - * /bar.gif if the file is at the top level). + * (e.g. com/foo/bar/foo.gif). */ public static InputStream getStream (String path) { @@ -204,18 +506,14 @@ public class ConfigUtil /** * Returns an input stream referencing a file that exists somewhere in - * the classpath. A full path (relative to the classpath directories) - * should be supplied, but variations including and not including a - * leading slash will be used because JVMs differ on their opinion of - * whether this is necessary. + * the classpath. * *

The supplied classloader is searched first, followed by the * system classloader. * * @param path The path to the file, relative to the root of the * classpath directory from which it will be loaded - * (e.g. /conf/foo.gif or perhaps just - * /bar.gif if the file is at the top level). + * (e.g. com/foo/bar/foo.gif). */ public static InputStream getStream (String path, ClassLoader loader) { @@ -280,4 +578,53 @@ public class ConfigUtil return "/" + path; } } + + /** Used when parsing inherited properties. */ + protected static final class PropRecord extends ArrayList + { + public String _package; + public String[] _overrides; + public String _extends; + + public String path; + public URL sourceURL; + + public PropRecord (String path, URL sourceURL) + { + this.path = path; + this.sourceURL = sourceURL; + } + + public void validate () + throws IOException + { + if (StringUtil.blank(_package)) { + throw new IOException( + "Properties file missing '_package' identifier " + this); + } + + if ((_overrides != null && _overrides.length > 0) && + (!StringUtil.blank(_extends))) { + throw new IOException("Properties file cannot use both " + + "'_overrides' and '_extends' " + this); + } + } + + public boolean equals (Object other) + { + return _package.equals(((PropRecord)other)._package); + } + + public String toString () + { + return "[package=" + _package + + ", overrides=" + StringUtil.toString(_overrides) + + ", extends=" + _extends + ", path=" + path + + ", source=" + sourceURL + "]"; + } + } + + protected static final String PACKAGE_KEY = "_package"; + protected static final String OVERRIDES_KEY = "_overrides"; + protected static final String EXTENDS_KEY = "_extends"; }