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;
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).
*
* <p> The resource manager can be provided with config properties at
* construct time, or it can load them via {@link #getResource} with a
* path of <code>config/resource/manager.properties</code>. 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:
* <p> 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:
*
* <pre>
* 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
* </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
* <code>resource.set.</code> and all text following that string is
* 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
* searched for resources is a call to {@link #getResource}.
*
* <p> 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.
* <p> 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 <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
* 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 <code>resources</code> 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
* <code>resource_url</code> 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";
}