Modified the resource manager to load resource sets not from the file

system but from a URL into a local cache. It presently loads all of the
resources every time the application starts. This will eventually be made
to be more sophisticated.


git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@862 542714f4-19e9-0310-aa3c-eee0fc999fb1
This commit is contained in:
Michael Bayne
2002-01-16 03:00:06 +00:00
parent b88b5c61e5
commit d4e2058ea6
@@ -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; package com.threerings.resource;
import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.FilenameFilter; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@@ -19,7 +18,8 @@ import java.util.HashMap;
import java.util.Properties; import java.util.Properties;
import java.util.StringTokenizer; 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 * 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 * 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 via the classloader (which will search the classpath).
* *
* <p> The resource manager can be provided with config properties at * <p> The resource manager must be provided with the URL of a resource
* construct time, or it can load them via {@link #getResource} with a * definition file which describes these resource sets at construct
* path of <code>config/resource/manager.properties</code>. The config * time. The definition file will be loaded and the resource bundles
* properties should contain resource set definitions for the default * defined within will be loaded relative to the resource definition URL.
* resource set and for any named resource sets needed by the application. * The bundles will be cached in the user's home directory and only
* An example configuration follows: * reloaded when the source resources have been updated. The resource
* definition file looks something like the following:
* *
* <pre> * <pre>
* resource.set.default = rsrc/sets/misc * resource.set.default = sets/misc/config.jar: \
* resource.set.tiles = rsrc/sets/tiles:/global/resources/tiles: \ * sets/misc/icons.jar
* /home/mdb/test_tiles.jar * resource.set.tiles = sets/tiles/ground.jar: \
* resource.set.sounds = rsrc/sets/sounds:/global/resources/sounds * 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
* </pre> * </pre>
* *
* 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.
*
* <p> All resource set definitions are prefixed with * <p> All resource set definitions are prefixed with
* <code>resource.set.</code> and all text following that string is * <code>resource.set.</code> and all text following that string is
* considered to be the name of the resource set. The resource set named * considered to be the name of the resource set. The resource set named
* <code>default</code> is the default resource set and is the one that is * <code>default</code> is the default resource set and is the one that is
* searched for resources is a call to {@link #getResource}. * searched for resources is a call to {@link #getResource}.
* *
* <p> Resource set definitions can contain directories or individual jar * <p> When a resource is loaded from a resource set, the set is searched
* files, the latter are simply added to the resource set; for the former, * in the order that entries are specified in the definition.
* 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.
*/ */
public class ResourceManager public class ResourceManager
{ {
/** /**
* Constructs a resource manager with the supplied application and * Constructs a resource manager which will load resources as
* resource roots. The resource manager configuration is loaded via * specified in the configuration file, the path to which is supplied
* the resource root (see class documentation for details). * via <code>configPath</code>. If resource sets are not needed and
* resources will only be loaded via the classpath, null may be passed
* in <code>resourceURL</code> and <code>configPath</code>.
* *
* @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
* <code>application.root</code> system property.
* @param resourceRoot the path to prepend to resource paths prior to * @param resourceRoot the path to prepend to resource paths prior to
* attempting to load them via the classloader. This is not a platform * attempting to load them via the classloader. When resources are
* dependent path. * 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
public ResourceManager (String appRoot, String resourceRoot) * that they'll live in some sort of <code>resources</code> directory
{ * to isolate them from the rest of the files in the classpath. This
this(appRoot, resourceRoot, null); * 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
* Constructs a resource manager with the supplied application and * loaded relative to this path. If this is null, the system property
* resource roots. * <code>resource_url</code> will be used, if available.
* * @param configPath the path (relative to the resource URL) of the
* @param appRoot the path to the application root directory. This is * resource definition file.
* 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.
*/ */
public ResourceManager ( public ResourceManager (
String appRoot, String resourceRoot, Properties config) String resourceRoot, String resourceURL, String configPath)
{ {
// keep track of our root path // keep track of our root path
_rootPath = resourceRoot; _rootPath = resourceRoot;
@@ -118,50 +103,43 @@ public class ResourceManager
_rootPath = _rootPath + "/"; _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 // use the classloader that loaded us
_loader = getClass().getClassLoader(); _loader = getClass().getClassLoader();
// load up our configuration if it wasn't supplied by the caller // if the resource URL wasn't provided, we try to figure it out
try { // for ourselves
if (config == null) { if (resourceURL == null) {
config = new Properties(); try {
config.load(getResource(CONFIG_PATH)); // 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(); Enumeration names = config.propertyNames();
while (names.hasMoreElements()) { while (names.hasMoreElements()) {
String key = (String)names.nextElement(); String key = (String)names.nextElement();
@@ -169,70 +147,166 @@ public class ResourceManager
continue; continue;
} }
String setName = key.substring(RESOURCE_SET_PREFIX.length()); 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 * Loads up a resource set based on the supplied definition
* information. * information.
*/ */
protected void resolveResourceSet ( protected void resolveResourceSet (
String appRoot, String name, String definition) URL resourceURL, String setName, String definition)
{ {
StringTokenizer tok = StringTokenizer tok = new StringTokenizer(definition, ":");
new StringTokenizer(definition, File.pathSeparator);
ArrayList set = new ArrayList(); ArrayList set = new ArrayList();
while (tok.hasMoreTokens()) { while (tok.hasMoreTokens()) {
// obtain the path and fully qualify it
String path = tok.nextToken().trim(); String path = tok.nextToken().trim();
if (!path.startsWith(File.separator)) { URL burl = null;
path = appRoot + path;
}
try { 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 // make sure the cache directory exists for this set
if (!efile.isDirectory()) { createCacheDirectory(setName);
set.add(new ResourceBundle(efile));
continue;
}
// it is a directory, so we have to add all of its entries // compute the path to the cache file for this bundle
// to the bundle File cfile = new File(genCachePath(setName, path));
File[] efiles = efile.listFiles(new FilenameFilter() {
public boolean accept (File dir, String filename) {
// only worry about .jar files
return filename.endsWith(".jar");
}
});
if (efiles == null) { Log.info("Cached to " + cfile.getPath());
Log.warning("Failure enumerating jars in directory " +
"[path=" + path + "].");
continue;
}
// phew, we made it // download the resource bundle from the specified URL
for (int i = 0; i < efiles.length; i++) { InputStream in = burl.openStream();
set.add(new ResourceBundle(efiles[i])); 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) { } catch (IOException ioe) {
Log.warning("Error processing resource set entry " + 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 // convert our array list into an array and stick it in the table
ResourceBundle[] setvec = new ResourceBundle[set.size()]; ResourceBundle[] setvec = new ResourceBundle[set.size()];
set.toArray(setvec); set.toArray(setvec);
_sets.put(name, setvec); _sets.put(setName, setvec);
// if this is our default resource bundle, keep a reference to it // 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; _default = setvec;
} }
} }
@@ -290,24 +364,22 @@ public class ResourceManager
* them from the classpath. */ * them from the classpath. */
protected String _rootPath; protected String _rootPath;
/** The path to our bundle cache directory. */
protected String _cachePath;
/** Our default resource set. */ /** Our default resource set. */
protected ResourceBundle[] _default = new ResourceBundle[0]; protected ResourceBundle[] _default = new ResourceBundle[0];
/** A table of our resource sets. */ /** A table of our resource sets. */
protected HashMap _sets = new HashMap(); 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 /** The prefix of configuration entries that describe a resource
* set. */ * set. */
protected static final String RESOURCE_SET_PREFIX = "resource.set."; protected static final String RESOURCE_SET_PREFIX = "resource.set.";
/** The name of the default resource set. */ /** The name of the default resource set. */
protected static final String DEFAULT_RESOURCE_SET = "default"; protected static final String DEFAULT_RESOURCE_SET = "default";
/** The name of our resource bundle cache directory. */
protected static final String CACHE_PATH = ".naryarsrc";
} }