Files
narya/src/java/com/threerings/resource/ResourceManager.java
T
Michael Bayne d35e7b8180 Change the default resource directory to .narya so that we can leave the
code in that blows away .naryarsrc without having it constantly conflict
with the tools that we run.


git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@2626 542714f4-19e9-0310-aa3c-eee0fc999fb1
2003-05-29 16:35:59 +00:00

721 lines
26 KiB
Java

//
// $Id: ResourceManager.java,v 1.30 2003/05/29 16:35:59 mdb Exp $
package com.threerings.resource;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.StringTokenizer;
import javax.imageio.stream.FileImageInputStream;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.MemoryCacheImageInputStream;
import com.samskivert.util.StringUtil;
import com.threerings.resource.DownloadManager.DownloadDescriptor;
import com.threerings.resource.DownloadManager.DownloadObserver;
/**
* The resource manager is responsible for maintaining a repository of
* resources that are synchronized with a remote source. This is
* accomplished in the form of sets of jar files (resource bundles) that
* contain resources and that are updated from a remote resource
* repository via HTTP. These resource bundles are organized into
* resource sets. A resource set contains one or more resource bundles and
* is defined much like a classpath.
*
* <p> The resource manager can load resources from the default resource
* set, and can make available named resource sets to entities that wish
* to do their own resource loading. If the resource manager fails to
* locate a resource in the default resource set, it falls back to loading
* the resource via the classloader (which will search the classpath).
*
* <p> Applications that wish to make use of resource sets and their
* associated bundles must call {@link #initBundles} after constructing
* the resource manager, providing the URL of a resource definition file
* which describes these resource sets. 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 = 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>
*
* <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> 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
{
/**
* Provides facilities for notifying an observer of resource bundle
* download progress.
*/
public interface BundleDownloadObserver
{
/**
* If this method returns true the download observer callbacks
* will be called on the AWT thread, allowing the observer to do
* things like safely update user interfaces, etc. If false, it
* will be called on a special download thread.
*/
public boolean notifyOnAWTThread ();
/**
* Called when the resource manager is about to check for an
* update of any of our resource sets.
*/
public void checkingForUpdate ();
/**
* Called to inform the observer of ongoing progress toward
* completion of the overall bundle downloading task. The caller
* is guaranteed to get at least one call reporting 100%
* completion.
*
* @param percent the percent completion of the download.
* @param remaining the estimated download time remaining in
* seconds, or <code>-1</code> if the time can not yet be
* determined.
*/
public void downloadProgress (int percent, long remaining);
/**
* Called following the call to {@link #dowloadProgress} of 100
* percent completion to indicate that we have validated and
* unpacked our bundles and are fully ready to go.
*/
public void downloadComplete ();
/**
* Called if a failure occurs while checking for an update or
* downloading all resource sets.
*/
public void downloadFailed (Exception e);
}
/**
* Constructs a resource manager which will load resources via the
* classloader, prepending <code>resourceRoot</code> to their path.
*
* @param resourceRoot the path to prepend to resource paths prior to
* 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).
*/
public ResourceManager (String resourceRoot)
{
// keep track of our root path
_rootPath = resourceRoot;
// make root path end with a slash (not the platform dependent
// file system separator character as resource paths are passed to
// ClassLoader.getResource() which requires / as its separator)
if (!_rootPath.endsWith("/")) {
_rootPath = _rootPath + "/";
}
// use the classloader that loaded us
_loader = getClass().getClassLoader();
// set up a URL handler so that things can be loaded via
// urls with the 'resource' protocol
Handler.registerHandler(this);
// sort our our default cache path
_baseCachePath = "";
try {
// first check for an explicitly specified cache directory
_baseCachePath = System.getProperty("narya.resource.cache_dir");
// if that's null, try putting it into their home directory
if (_baseCachePath == null) {
_baseCachePath = System.getProperty("user.home");
}
if (!_baseCachePath.endsWith(File.separator)) {
_baseCachePath += File.separator;
}
_baseCachePath += (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 + "].");
}
}
/**
* Instructs the resource manager to store cached resources in the
* specified directory. This must be called before {@link
* #initBundles} if it is going to be called at all. If it is not
* called, the default cache directory will be used.
*/
public void setCachePath (String cachePath)
{
if (!cachePath.endsWith(File.separator)) {
cachePath += File.separator;
}
_baseCachePath = cachePath;
}
/**
* Initializes the bundle sets to be made available by this resource
* manager. Applications that wish to make use of resource bundles
* should call this method after constructing the resource manager.
*
* @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.
* @param downloadObs the bundle download observer to notify of
* download progress and success or failure, or <code>null</code> if
* the caller doesn't care to be informed; note that in the latter
* case, the calling thread will block until bundle updating is
* complete.
*/
public void initBundles (String resourceURL, String configPath,
BundleDownloadObserver downloadObs)
{
// 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;
}
}
// 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
ArrayList dlist = new ArrayList();
Enumeration names = config.propertyNames();
while (names.hasMoreElements()) {
String key = (String)names.nextElement();
if (!key.startsWith(RESOURCE_SET_PREFIX)) {
continue;
}
String setName = key.substring(RESOURCE_SET_PREFIX.length());
resolveResourceSet(rurl, setName, config.getProperty(key), dlist);
}
// start the download, blocking if we've no observer
DownloadManager dlmgr = new DownloadManager();
if (downloadObs == null) {
downloadBlocking(dlmgr, dlist);
} else {
downloadNonBlocking(dlmgr, dlist, downloadObs);
}
}
/**
* Downloads the files in the supplied download list, blocking the
* calling thread until the download is complete or a failure has
* occurred.
*/
protected void downloadBlocking (DownloadManager dlmgr, List dlist)
{
// create an object to wait on while the download takes place
final Object lock = new Object();
// create the observer that will notify us when all is finished
DownloadObserver obs = new DownloadObserver() {
public boolean notifyOnAWTThread () {
return false;
}
public void resolvingDownloads () {
// nothing for now
}
public void downloadProgress (int percent, long remaining) {
// nothing for now
}
public void postDownloadHook () {
bundlesDownloaded();
}
public void downloadComplete () {
synchronized (lock) {
// wake things up as the download is finished
lock.notify();
}
}
public void downloadFailed (DownloadDescriptor desc, Exception e) {
Log.warning("Failed to download file " +
"[desc=" + desc + ", e=" + e + "].");
synchronized (lock) {
// wake things up since we're fragile and so a
// single failure means all is booched
lock.notify();
}
}
};
synchronized (lock) {
// pass the descriptors on to the download manager
dlmgr.download(dlist, true, obs);
try {
// block until the download has completed
lock.wait();
} catch (InterruptedException ie) {
Log.warning("Thread interrupted while waiting for download " +
"to complete [ie=" + ie + "].");
}
}
}
/**
* Downloads the files in the supplied download list asynchronously,
* notifying the download observer of ongoing progress.
*/
protected void downloadNonBlocking (
DownloadManager dlmgr, List dlist, final BundleDownloadObserver obs)
{
// pass the descriptors on to the download manager
dlmgr.download(dlist, true, new DownloadObserver() {
public boolean notifyOnAWTThread () {
return obs.notifyOnAWTThread();
}
public void resolvingDownloads () {
obs.checkingForUpdate();
}
public void downloadProgress (int percent, long remaining) {
obs.downloadProgress(percent, remaining);
}
public void postDownloadHook () {
bundlesDownloaded();
}
public void downloadComplete () {
obs.downloadComplete();
}
public void downloadFailed (DownloadDescriptor desc, Exception e) {
obs.downloadFailed(e);
}
});
}
/**
* Called when our resource bundle downloads have completed.
*/
protected void bundlesDownloaded ()
{
// let our bundles know that it's ok for them to access their
// resource files
Iterator iter = _sets.values().iterator();
while (iter.hasNext()) {
ResourceBundle[] bundles = (ResourceBundle[])iter.next();
for (int ii = 0; ii < bundles.length; ii++) {
bundles[ii].sourceIsReady();
}
}
}
/**
* 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 (URL resourceURL, String setName)
{
// compute the path to the top-level cache directory if we haven't
// already been so configured
if (_cachePath == null) {
// start with the base cache path
_cachePath = _baseCachePath;
if (!createDirectory(_cachePath)) {
return false;
}
// next incorporate the resource URL into the cache path so
// that files fetched from different resource roots do not
// overwrite one another
_cachePath += StringUtil.md5hex(resourceURL.toString()) +
File.separator;
if (!createDirectory(_cachePath)) {
return false;
}
Log.debug("Caching bundles in '" + _cachePath + "'.");
}
// 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.mkdirs()) {
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 (
URL resourceURL, 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 (
URL resourceURL, String setName, String definition, List dlist)
{
StringTokenizer tok = new StringTokenizer(definition, ":");
ArrayList set = new ArrayList();
while (tok.hasMoreTokens()) {
String path = tok.nextToken().trim();
URL burl = null;
try {
burl = new URL(resourceURL, path);
// make sure the cache directory exists for this set
createCacheDirectory(resourceURL, setName);
// compute the path to the cache file for this bundle
File cfile = new File(genCachePath(resourceURL, setName, path));
// slap this on the list for retrieval or update by the
// download manager
dlist.add(new DownloadDescriptor(burl, cfile));
// finally, add the file that will be cached to the set as
// a resource bundle
set.add(new ResourceBundle(cfile, true, true));
} catch (MalformedURLException mue) {
Log.warning("Unable to create URL for resource " +
"[set=" + setName + ", path=" + path +
", error=" + mue + "].");
}
}
// convert our array list into an array and stick it in the table
ResourceBundle[] setvec = new ResourceBundle[set.size()];
set.toArray(setvec);
_sets.put(setName, setvec);
// if this is our default resource bundle, keep a reference to it
if (DEFAULT_RESOURCE_SET.equals(setName)) {
_default = setvec;
}
}
/**
* Fetches a resource from the local repository.
*
* @param path the path to the resource
* (ie. "config/miso.properties"). This should not begin with a slash.
*
* @exception IOException thrown if a problem occurs locating or
* reading the resource.
*/
public InputStream getResource (String path)
throws IOException
{
InputStream in = null;
// first look for this resource in our default resource bundle
for (int i = 0; i < _default.length; i++) {
in = _default[i].getResource(path);
if (in != null) {
return in;
}
}
// if we didn't find anything, try the classloader
String rpath = _rootPath + path;
in = _loader.getResourceAsStream(rpath);
if (in != null) {
return in;
}
// if we still haven't found it, we throw an exception
String errmsg = "Unable to locate resource [path=" + path + "]";
throw new FileNotFoundException(errmsg);
}
/**
* Fetches the specified resource as an {@link ImageInputStream} and
* one that takes advantage, if possible, of caching of unpacked
* resources on the local filesystem.
*
* @exception FileNotFoundException thrown if the resource could not
* be located in any of the bundles in the specified set, or if the
* specified set does not exist.
* @exception IOException thrown if a problem occurs locating or
* reading the resource.
*/
public ImageInputStream getImageResource (String path)
throws IOException
{
// first look for this resource in our default resource bundle
for (int i = 0; i < _default.length; i++) {
File file = _default[i].getResourceFile(path);
if (file != null) {
return new FileImageInputStream(file);
}
}
// if we didn't find anything, try the classloader
String rpath = _rootPath + path;
InputStream in = _loader.getResourceAsStream(rpath);
if (in != null) {
return new MemoryCacheImageInputStream(new BufferedInputStream(in));
}
// if we still haven't found it, we throw an exception
String errmsg = "Unable to locate image resource [path=" + path + "]";
throw new FileNotFoundException(errmsg);
}
/**
* Returns an input stream from which the requested resource can be
* loaded. <em>Note:</em> this performs a linear search of all of the
* bundles in the set and returns the first resource found with the
* specified path, thus it is not extremely efficient and will behave
* unexpectedly if you use the same paths in different resource
* bundles.
*
* @exception FileNotFoundException thrown if the resource could not
* be located in any of the bundles in the specified set, or if the
* specified set does not exist.
* @exception IOException thrown if a problem occurs locating or
* reading the resource.
*/
public InputStream getResource (String rset, String path)
throws IOException
{
// grab the resource bundles in the specified resource set
ResourceBundle[] bundles = getResourceSet(rset);
if (bundles == null) {
throw new FileNotFoundException(
"Unable to locate resource [set=" + rset +
", path=" + path + "]");
}
// look for the resource in any of the bundles
int size = bundles.length;
for (int ii = 0; ii < size; ii++) {
InputStream instr = bundles[ii].getResource(path);
if (instr != null) {
// Log.info("Found resource [rset=" + rset +
// ", bundle=" + bundles[ii].getSource().getPath() +
// ", path=" + path + ", in=" + instr + "].");
return instr;
}
}
throw new FileNotFoundException(
"Unable to locate resource [set=" + rset + ", path=" + path + "]");
}
/**
* Fetches the specified resource as an {@link ImageInputStream} and
* one that takes advantage, if possible, of caching of unpacked
* resources on the local filesystem.
*
* @exception FileNotFoundException thrown if the resource could not
* be located in any of the bundles in the specified set, or if the
* specified set does not exist.
* @exception IOException thrown if a problem occurs locating or
* reading the resource.
*/
public ImageInputStream getImageResource (String rset, String path)
throws IOException
{
// grab the resource bundles in the specified resource set
ResourceBundle[] bundles = getResourceSet(rset);
if (bundles == null) {
throw new FileNotFoundException(
"Unable to locate image resource [set=" + rset +
", path=" + path + "]");
}
// look for the resource in any of the bundles
int size = bundles.length;
for (int ii = 0; ii < size; ii++) {
File file = bundles[ii].getResourceFile(path);
if (file != null) {
// Log.info("Found image resource [rset=" + rset +
// ", bundle=" + bundles[ii].getSource() +
// ", path=" + path + ", file=" + file + "].");
return new FileImageInputStream(file);
}
}
String errmsg = "Unable to locate image resource [set=" + rset +
", path=" + path + "]";
throw new FileNotFoundException(errmsg);
}
/**
* Returns a reference to the resource set with the specified name, or
* null if no set exists with that name. Services that wish to load
* their own resources can allow the resource manager to load up a
* resource set for them, from which they can easily load their
* resources.
*/
public ResourceBundle[] getResourceSet (String name)
{
return (ResourceBundle[])_sets.get(name);
}
/** The classloader we use for classpath-based resource loading. */
protected ClassLoader _loader;
/** The prefix we prepend to resource paths before attempting to load
* them from the classpath. */
protected String _rootPath;
/** The path to the directory that will contain our bundle cache. */
protected String _baseCachePath;
/** 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 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 = ".narya";
}