diff --git a/src/java/com/threerings/resource/ResourceManager.java b/src/java/com/threerings/resource/ResourceManager.java index 0619feeb4..f69531e13 100644 --- a/src/java/com/threerings/resource/ResourceManager.java +++ b/src/java/com/threerings/resource/ResourceManager.java @@ -1,12 +1,11 @@ // -// $Id: ResourceManager.java,v 1.6 2001/11/27 08:09:35 mdb Exp $ +// $Id: ResourceManager.java,v 1.7 2002/01/16 03:00:06 mdb Exp $ package com.threerings.resource; -import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileNotFoundException; -import java.io.FilenameFilter; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; @@ -19,7 +18,8 @@ import java.util.HashMap; import java.util.Properties; import java.util.StringTokenizer; -import com.samskivert.Log; +import org.apache.commons.util.StreamUtils; +import com.samskivert.util.StringUtil; /** * The resource manager is responsible for maintaining a repository of @@ -36,77 +36,62 @@ import com.samskivert.Log; * locate a resource in the default resource set, it falls back to loading * the resource via the classloader (which will search the classpath). * - *
The resource manager can be provided with config properties at
- * construct time, or it can load them via {@link #getResource} with a
- * path of config/resource/manager.properties. The config
- * properties should contain resource set definitions for the default
- * resource set and for any named resource sets needed by the application.
- * An example configuration follows:
+ *
The resource manager must be provided with the URL of a resource + * definition file which describes these resource sets at construct + * time. The definition file will be loaded and the resource bundles + * defined within will be loaded relative to the resource definition URL. + * The bundles will be cached in the user's home directory and only + * reloaded when the source resources have been updated. The resource + * definition file looks something like the following: * *
- * resource.set.default = rsrc/sets/misc - * resource.set.tiles = rsrc/sets/tiles:/global/resources/tiles: \ - * /home/mdb/test_tiles.jar - * resource.set.sounds = rsrc/sets/sounds:/global/resources/sounds + * resource.set.default = sets/misc/config.jar: \ + * sets/misc/icons.jar + * resource.set.tiles = sets/tiles/ground.jar: \ + * sets/tiles/objects.jar: \ + * /global/resources/tiles/ground.jar: \ + * /global/resources/tiles/objects.jar + * resource.set.sounds = sets/sounds/sfx.jar: \ + * sets/sounds/music.jar: \ + * /global/resources/sounds/sfx.jar: \ + * /global/resources/sounds/music.jar ** - * Platform-specific file and path separators should be used in the - * resource set definitions as these are actual file paths. If a path - * component starts with a file separator, it will be interpreted as an - * absolute path, whereas if it doesn't, it will be interpreted as - * relative to the application root that was supplied to the resource - * manager at construct time. - * *
All resource set definitions are prefixed with
* resource.set. and all text following that string is
* considered to be the name of the resource set. The resource set named
* default is the default resource set and is the one that is
* searched for resources is a call to {@link #getResource}.
*
- *
Resource set definitions can contain directories or individual jar - * files, the latter are simply added to the resource set; for the former, - * all jar files in the specified directory (but not its subdirectories) - * are added to the set. When a resource is loaded from a resource set, - * the set is searched in the order that entries are specified in the - * definition (the left-most entry first, and so on). Jar files in a - * directory are added to the set (and thus, searched) in alphabetical - * order. + *
When a resource is loaded from a resource set, the set is searched
+ * in the order that entries are specified in the definition.
*/
public class ResourceManager
{
/**
- * Constructs a resource manager with the supplied application and
- * resource roots. The resource manager configuration is loaded via
- * the resource root (see class documentation for details).
+ * Constructs a resource manager which will load resources as
+ * specified in the configuration file, the path to which is supplied
+ * via configPath. If resource sets are not needed and
+ * resources will only be loaded via the classpath, null may be passed
+ * in resourceURL and configPath.
*
- * @param appRoot the path to the application root directory. This is
- * a platform dependent path and should contain separator characters
- * proper to the host platform. If null, this will be obtained via the
- * application.root system property.
* @param resourceRoot the path to prepend to resource paths prior to
- * attempting to load them via the classloader. This is not a platform
- * dependent path.
- */
- public ResourceManager (String appRoot, String resourceRoot)
- {
- this(appRoot, resourceRoot, null);
- }
-
- /**
- * Constructs a resource manager with the supplied application and
- * resource roots.
- *
- * @param appRoot the path to the application root directory. This is
- * a platform dependent path and should contain separator characters
- * proper to the host platform.
- * @param resourceRoot the path to prepend to resource paths prior to
- * attempting to load them via the classloader. This is not a platform
- * dependent path.
- * @param config the configuration for this resource manager. See
- * class documentation for a description of the config properties.
+ * attempting to load them via the classloader. When resources are
+ * bundled into the default resource bundle, they don't need this
+ * prefix, but if they're to be loaded from the classpath, it's likely
+ * that they'll live in some sort of resources directory
+ * to isolate them from the rest of the files in the classpath. This
+ * is not a platform dependent path (forward slash is always used to
+ * separate path elements).
+ * @param resourceURL the base URL from which resources are loaded.
+ * Relative paths specified in the resource definition file will be
+ * loaded relative to this path. If this is null, the system property
+ * resource_url will be used, if available.
+ * @param configPath the path (relative to the resource URL) of the
+ * resource definition file.
*/
public ResourceManager (
- String appRoot, String resourceRoot, Properties config)
+ String resourceRoot, String resourceURL, String configPath)
{
// keep track of our root path
_rootPath = resourceRoot;
@@ -118,50 +103,43 @@ public class ResourceManager
_rootPath = _rootPath + "/";
}
- // get our app root if we weren't provided with one
- if (appRoot == null) {
- try {
- appRoot = System.getProperty(APP_ROOT_PROPERTY);
- } catch (SecurityException se) {
- // we may be running in an applet, so we'll calmly ignore
- // this little freakout
- }
-
- // if it's still null, we complain loudly
- if (appRoot == null) {
- Log.warning("No application root provided to resource " +
- "manager. Assuming current working directory.");
- appRoot = "";
- }
- }
-
- // make the app root end with a file separator (unless we're
- // rolling with an empty app root)
- if ((appRoot.length() > 0) && !appRoot.endsWith(File.separator)) {
- appRoot = appRoot + File.separator;
- }
-
// use the classloader that loaded us
_loader = getClass().getClassLoader();
- // load up our configuration if it wasn't supplied by the caller
- try {
- if (config == null) {
- config = new Properties();
- config.load(getResource(CONFIG_PATH));
+ // if the resource URL wasn't provided, we try to figure it out
+ // for ourselves
+ if (resourceURL == null) {
+ try {
+ // first look for the explicit system property
+ resourceURL = System.getProperty("resource_url");
+
+ // if that doesn't work, fall back to the current directory
+ if (resourceURL == null) {
+ resourceURL = "file:" + System.getProperty("user.dir");
+ }
+
+ } catch (SecurityException se) {
+ resourceURL = "file:" + File.separator;
}
-
- } catch (FileNotFoundException fnfe) {
- // nothing to worry about here, we aren't required to have a
- // configuration
-
- } catch (IOException ioe) {
- // complain if some other error occurs
- Log.warning("Error loading resource manager configuration " +
- "[path=" + CONFIG_PATH + ", error=" + ioe + "].");
}
- // load up any configured resource sets
+ // make sure there's a slash at the end of the URL
+ if (!resourceURL.endsWith("/")) {
+ resourceURL += "/";
+ }
+
+ URL rurl = null;
+ try {
+ rurl = new URL(resourceURL);
+ } catch (MalformedURLException mue) {
+ Log.warning("Invalid resource URL [url=" + resourceURL +
+ ", error=" + mue + "].");
+ }
+
+ // load up our configuration
+ Properties config = loadConfig(rurl, configPath);
+
+ // resolve the configured resource sets
Enumeration names = config.propertyNames();
while (names.hasMoreElements()) {
String key = (String)names.nextElement();
@@ -169,70 +147,166 @@ public class ResourceManager
continue;
}
String setName = key.substring(RESOURCE_SET_PREFIX.length());
- resolveResourceSet(appRoot, setName, config.getProperty(key));
+ resolveResourceSet(rurl, setName, config.getProperty(key));
}
}
+ /**
+ * Loads up the most recent version of the resource manager
+ * configuration.
+ */
+ protected Properties loadConfig (URL resourceURL, String configPath)
+ {
+ Properties config = new Properties();
+ URL curl = null;
+
+ try {
+ if (configPath != null) {
+ curl = new URL(resourceURL, configPath);
+ config.load(curl.openStream());
+ }
+
+ } catch (MalformedURLException mue) {
+ Log.warning("Unable to construct config URL " +
+ "[resourceURL=" + resourceURL +
+ ", configPath=" + configPath +
+ ", error=" + mue + "].");
+
+ } catch (IOException ioe) {
+ // complain if some other error occurs
+ Log.warning("Error loading resource manager configuration " +
+ "[url=" + curl + ", error=" + ioe + "].");
+ }
+
+ return config;
+ }
+
+ /**
+ * Ensures that the cache directory for the specified resource set is
+ * created.
+ *
+ * @return true if the directory was successfully created (or was
+ * already there), false if we failed to create it.
+ */
+ protected boolean createCacheDirectory (String setName)
+ {
+ // get the path to the top-level cache directory if we don't
+ // already have it
+ if (_cachePath == null) {
+ try {
+ String dir = System.getProperty("user.home");
+ _cachePath = (dir + File.separator +
+ CACHE_PATH + File.separator);
+ } catch (SecurityException se) {
+ Log.info("Can't obtain user.home system property. Probably " +
+ "won't be able to create our cache directory " +
+ "either. [error=" + se + "].");
+ _cachePath = "";
+ }
+ }
+
+ // make sure the main cache directory exists
+ if (!createDirectory(_cachePath)) {
+ return false;
+ }
+
+ // ensure that the set-specific cache directory exists
+ return createDirectory(_cachePath + setName);
+ }
+
+ /**
+ * Creates the specified directory if it doesn't already exist.
+ *
+ * @return true if directory was created (or existed), false if not.
+ */
+ protected boolean createDirectory (String path)
+ {
+ File cdir = new File(path);
+ if (cdir.exists()) {
+ if (!cdir.isDirectory()) {
+ Log.warning("Cache dir exists but isn't a directory?! " +
+ "[path=" + path + "].");
+ return false;
+ }
+
+ } else {
+ if (!cdir.mkdir()) {
+ Log.warning("Unable to create cache dir. " +
+ "[path=" + path + "].");
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Generates the name of the bundle cache file given the name of the
+ * resource set to which it belongs and the relative path URL.
+ */
+ protected String genCachePath (String setName, String resourcePath)
+ {
+ return _cachePath + setName + File.separator +
+ StringUtil.replace(resourcePath, "/", "-");
+ }
+
/**
* Loads up a resource set based on the supplied definition
* information.
*/
protected void resolveResourceSet (
- String appRoot, String name, String definition)
+ URL resourceURL, String setName, String definition)
{
- StringTokenizer tok =
- new StringTokenizer(definition, File.pathSeparator);
+ StringTokenizer tok = new StringTokenizer(definition, ":");
ArrayList set = new ArrayList();
while (tok.hasMoreTokens()) {
- // obtain the path and fully qualify it
String path = tok.nextToken().trim();
- if (!path.startsWith(File.separator)) {
- path = appRoot + path;
- }
+ URL burl = null;
try {
- File efile = new File(path);
+ burl = new URL(resourceURL, path);
+ Log.info("Resolving resource: " + burl);
- // if this isn't a directory, we assume it's a jar file
- if (!efile.isDirectory()) {
- set.add(new ResourceBundle(efile));
- continue;
- }
+ // make sure the cache directory exists for this set
+ createCacheDirectory(setName);
- // it is a directory, so we have to add all of its entries
- // to the bundle
- File[] efiles = efile.listFiles(new FilenameFilter() {
- public boolean accept (File dir, String filename) {
- // only worry about .jar files
- return filename.endsWith(".jar");
- }
- });
+ // compute the path to the cache file for this bundle
+ File cfile = new File(genCachePath(setName, path));
- if (efiles == null) {
- Log.warning("Failure enumerating jars in directory " +
- "[path=" + path + "].");
- continue;
- }
+ Log.info("Cached to " + cfile.getPath());
- // phew, we made it
- for (int i = 0; i < efiles.length; i++) {
- set.add(new ResourceBundle(efiles[i]));
- }
+ // download the resource bundle from the specified URL
+ InputStream in = burl.openStream();
+ FileOutputStream out = new FileOutputStream(cfile);
+
+ // pipe the input stream into the output stream
+ StreamUtils.pipe(in, out);
+ in.close();
+ out.close();
+
+ // finally add this newly cached file to the set as a
+ // resource bundle
+ set.add(new ResourceBundle(cfile));
+
+ } catch (MalformedURLException mue) {
+ Log.warning("Unable to create URL for resource " +
+ "[set=" + setName + ", path=" + path +
+ ", error=" + mue + "].");
} catch (IOException ioe) {
Log.warning("Error processing resource set entry " +
- "[entry=" + path + ", error=" + ioe + "].");
+ "[url=" + burl + ", error=" + ioe + "].");
}
}
// convert our array list into an array and stick it in the table
ResourceBundle[] setvec = new ResourceBundle[set.size()];
set.toArray(setvec);
- _sets.put(name, setvec);
+ _sets.put(setName, setvec);
// if this is our default resource bundle, keep a reference to it
- if (DEFAULT_RESOURCE_SET.equals(name)) {
+ if (DEFAULT_RESOURCE_SET.equals(setName)) {
_default = setvec;
}
}
@@ -290,24 +364,22 @@ public class ResourceManager
* them from the classpath. */
protected String _rootPath;
+ /** The path to our bundle cache directory. */
+ protected String _cachePath;
+
/** Our default resource set. */
protected ResourceBundle[] _default = new ResourceBundle[0];
/** A table of our resource sets. */
protected HashMap _sets = new HashMap();
- /** The system property from which we load our application root. */
- protected static final String APP_ROOT_PROPERTY = "application.root";
-
- /** The path to the resource manager config file (which will be loaded
- * via the classloader). */
- protected static final String CONFIG_PATH =
- "config/resource/manager.properties";
-
/** The prefix of configuration entries that describe a resource
* set. */
protected static final String RESOURCE_SET_PREFIX = "resource.set.";
/** The name of the default resource set. */
protected static final String DEFAULT_RESOURCE_SET = "default";
+
+ /** The name of our resource bundle cache directory. */
+ protected static final String CACHE_PATH = ".naryarsrc";
}