// // $Id$ // // Nenya library - tools for developing networked games // Copyright (C) 2002-2007 Three Rings Design, Inc., All Rights Reserved // http://www.threerings.net/code/nenya/ // // This library is free software; you can redistribute it and/or modify it // under the terms of the GNU Lesser General Public License as published // by the Free Software Foundation; either version 2.1 of the License, or // (at your option) any later version. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA package com.threerings.resource; import java.awt.EventQueue; import java.awt.image.BufferedImage; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Properties; import java.util.StringTokenizer; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.security.AccessController; import java.security.PrivilegedAction; import javax.imageio.IIOException; import javax.imageio.ImageIO; import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.MemoryCacheImageInputStream; import com.samskivert.io.StreamUtil; import com.samskivert.net.PathUtil; import com.samskivert.util.ResultListener; import com.samskivert.util.StringUtil; /** * 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. * *
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). * *
Applications that wish to make use of resource sets and their associated bundles must call * {@link #initBundles} after constructing the resource manager, providing the path to 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 directory. 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 = 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 ** *
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}.
*
*
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 the resource unpacking process.
*/
public interface InitObserver
{
/**
* Indicates a percent completion along with an estimated time remaining in seconds.
*/
public void progress (int percent, long remaining);
/**
* Indicates that there was a failure unpacking our resource bundles.
*/
public void initializationFailed (Exception e);
}
/**
* An adapter that wraps an {@link InitObserver} and routes all method invocations to the AWT
* thread.
*/
public static class AWTInitObserver implements InitObserver
{
public AWTInitObserver (InitObserver obs) {
_obs = obs;
}
public void progress (final int percent, final long remaining) {
EventQueue.invokeLater(new Runnable() {
public void run () {
_obs.progress(percent, remaining);
}
});
}
public void initializationFailed (final Exception e) {
EventQueue.invokeLater(new Runnable() {
public void run () {
_obs.initializationFailed(e);
}
});
}
protected InitObserver _obs;
}
/**
* Constructs a resource manager which will load resources via the classloader, prepending
* resourceRoot 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 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).
*/
public ResourceManager (String resourceRoot)
{
this(resourceRoot, ResourceManager.class.getClassLoader());
}
/**
* Creates a resource manager with the specified class loader via which to load classes. See
* {@link #ResourceManager(String)} for further documentation.
*/
public ResourceManager (String resourceRoot, ClassLoader loader)
{
this(resourceRoot, null, loader);
}
/**
* Creates a resource manager with a root path to resources over the network. See
* {@link #ResourceManager(String)} for further documentation.
*/
public ResourceManager (String resourceRoot, String networkResourceRoot)
{
this(resourceRoot, networkResourceRoot, ResourceManager.class.getClassLoader());
}
/**
* Creates a resource manager with a root path to resources over the network and the specified
* class loader via which to load classes. See {@link #ResourceManager(String)} for further
* documentation.
*/
public ResourceManager (String fileResourceRoot, String networkResourceRoot, ClassLoader loader)
{
_rootPath = fileResourceRoot;
_networkRootPath = networkResourceRoot;
_loader = loader;
// check a system property to determine if we should unpack our bundles, but don't freak
// out if we fail to read it
try {
_unpack = !Boolean.getBoolean("no_unpack_resources");
} catch (SecurityException se) {
// no problem, we're in a sandbox so we definitely won't be unpacking
}
// get our resource directory from resource_dir if possible
initResourceDir(null);
}
/**
* Registers a protocol handler with URL to handle resource: URLs. The URLs take
* the form:
resource://bundle_name/resource_pathResources from the default bundle * can be loaded via:
resource:///resource_path*/ public void activateResourceProtocol () { // set up a URL handler so that things can be loaded via urls with the 'resource' protocol try { AccessController.doPrivileged(new PrivilegedAction() { public Object run () { Handler.registerHandler(ResourceManager.this); return null; } }); } catch (SecurityException se) { Log.info("Running in sandbox. Unable to bind rsrc:// handler."); } } /** * Set where we should look for locale-specific resources. */ public void setLocalePrefix (String prefix) { _localePrefix = prefix; } /** * Configures whether we unpack our resource bundles or not. This must be called before {@link * #initBundles}. One can also pass the
-Dno_unpack_resources=true system property
* to disable resource unpacking.
*/
public void setUnpackResources (boolean unpackResources)
{
_unpack = unpackResources;
}
/**
* 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 resourceDir the base directory to which the paths in the supplied configuration file
* are relative. If this is null, the system property resource_dir will be used,
* if available.
* @param configPath the path (relative to the resource dir) of the resource definition file.
* @param initObs a bundle initialization observer to notify of unpacking progress and success
* or failure, or null if the caller doesn't care to be informed; note that in the
* latter case, the calling thread will block until bundle unpacking is complete.
*
* @exception IOException thrown if we are unable to read our resource manager configuration.
*/
public void initBundles (String resourceDir, String configPath, InitObserver initObs)
throws IOException
{
// reinitialize our resource dir if it was specified
if (resourceDir != null) {
initResourceDir(resourceDir);
}
// load up our configuration
Properties config = loadConfig(configPath);
// resolve the configured resource sets
Listnull to set the resource dir to
* the value of the resource_dir system property.
*/
public void initResourceDir (String resourceDir)
{
// if none was specified, check the resource_dir system property
if (resourceDir == null) {
try {
resourceDir = System.getProperty("resource_dir");
} catch (SecurityException se) {
// no problem
}
}
// if we found no resource directory, don't use one
if (resourceDir == null) {
return;
}
// make sure there's a trailing slash
if (!resourceDir.endsWith(File.separator)) {
resourceDir += File.separator;
}
_rdir = new File(resourceDir);
}
/**
* Given a path relative to the resource directory, the path is properly jimmied (assuming we
* always use /) and combined with the resource directory to yield a {@link File} object that
* can be used to access the resource.
*
* @return a file referencing the specified resource or null if the resource manager was never
* configured with a resource directory.
*/
public File getResourceFile (String path)
{
if (_rdir == null) {
return null;
}
if (!"/".equals(File.separator)) {
path = StringUtil.replace(path, "/", File.separator);
}
return new File(_rdir, path);
}
/**
* Checks to see if the specified bundle exists, is unpacked and is ready to be used.
*/
public boolean checkBundle (String path)
{
File bfile = getResourceFile(path);
return (bfile == null) ? false : new FileResourceBundle(bfile, true, _unpack).isUnpacked();
}
/**
* Resolve the specified bundle (the bundle file must already exist in the appropriate place on
* the file system) and return it on the specified result listener. Note that the result
* listener may be notified before this method returns on the caller's thread if the bundle is
* already resolved, or it may be notified on a brand new thread if the bundle requires
* unpacking.
*/
public void resolveBundle (String path, final ResultListener listener)
{
File bfile = getResourceFile(path);
if (bfile == null) {
String errmsg = "ResourceManager not configured with resource directory.";
listener.requestFailed(new IOException(errmsg));
return;
}
final FileResourceBundle bundle = new FileResourceBundle(bfile, true, _unpack);
if (bundle.isUnpacked()) {
if (bundle.sourceIsReady()) {
listener.requestCompleted(bundle);
} else {
String errmsg = "Bundle initialization failed.";
listener.requestFailed(new IOException(errmsg));
}
return;
}
// start a thread to unpack our bundles
ArrayList list = new ArrayList();
list.add(bundle);
Unpacker unpack = new Unpacker(list, new InitObserver() {
public void progress (int percent, long remaining) {
if (percent == 100) {
listener.requestCompleted(bundle);
}
}
public void initializationFailed (Exception e) {
listener.requestFailed(e);
}
});
unpack.start();
}
/**
* Returns the class loader being used to load resources if/when there are no resource bundles
* from which to load them.
*/
public ClassLoader getClassLoader ()
{
return _loader;
}
/**
* Configures the class loader this manager should use to load resources if/when there are no
* bundles from which to load them.
*/
public void setClassLoader (ClassLoader loader)
{
_loader = loader;
}
/**
* 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 (ResourceBundle bundle : _default) {
// Try a localized version first.
if (_localePrefix != null) {
in = bundle.getResource(PathUtil.appendPath(_localePrefix, path));
}
// If that didn't work, try generic.
if (in == null) {
in = bundle.getResource(path);
}
if (in != null) {
return in;
}
}
// fallback next to an unpacked resource file
File file = getResourceFile(path);
if (file != null && file.exists()) {
return new FileInputStream(file);
}
// if we still didn't find anything, try the classloader; first try a locale-specific file
if (_localePrefix != null) {
final String rpath = PathUtil.appendPath(
_rootPath, PathUtil.appendPath(_localePrefix, path));
in = (InputStream)AccessController.doPrivileged(new PrivilegedAction() {
public Object run () {
return _loader.getResourceAsStream(rpath);
}
});
if (in != null) {
return in;
}
}
// if we didn't find that, try locale-neutral
final String rpath = PathUtil.appendPath(_rootPath, path);
in = (InputStream)AccessController.doPrivileged(new PrivilegedAction() {
public Object run () {
return _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 and decodes the specified resource into a {@link BufferedImage}.
*
* @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 BufferedImage getImageResource (String path)
throws IOException
{
// first look for this resource in our default resource bundle
for (ResourceBundle bundle : _default) {
// try a localized version first
BufferedImage image = null;
if (_localePrefix != null) {
image =
bundle.getImageResource(PathUtil.appendPath(_localePrefix, path), false);
}
// if we didn't find that, try generic
if (image == null) {
image = bundle.getImageResource(path, false);
}
if (image != null) {
return image;
}
}
// fallback next to an unpacked resource file
File file = getResourceFile(path);
if (file != null && file.exists()) {
return loadImage(file, path.endsWith(FastImageIO.FILE_SUFFIX));
}
// first try a locale-specific file
if (_localePrefix != null) {
final String rpath = PathUtil.appendPath(
_rootPath, PathUtil.appendPath(_localePrefix, path));
InputStream in = (InputStream)AccessController.doPrivileged(new PrivilegedAction() {
public Object run () {
return _loader.getResourceAsStream(rpath);
}
});
if (in != null) {
return loadImage(in);
}
}
// if we still didn't find anything, try the classloader
final String rpath = PathUtil.appendPath(_rootPath, path);
InputStream in = (InputStream)AccessController.doPrivileged(new PrivilegedAction() {
public Object run () {
return _loader.getResourceAsStream(rpath);
}
});
if (in != null) {
return loadImage(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. Note: 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 = null;
// Try a localized version first.
if (_localePrefix != null) {
instr = bundles[ii].getResource(PathUtil.appendPath(_localePrefix, path));
}
// If we didn't find that, try a generic.
if (instr == null) {
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 and decodes the specified resource into a {@link BufferedImage}.
*
* @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 BufferedImage 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++) {
BufferedImage image = null;
// try a localized version first
if (_localePrefix != null) {
image =
bundles[ii].getImageResource(PathUtil.appendPath(_localePrefix, path), false);
}
// if we didn't find that, try generic
if (image == null) {
image = bundles[ii].getImageResource(path, false);
}
if (image != null) {
// Log.info("Found image resource [rset=" + rset +
// ", bundle=" + bundles[ii].getSource() + ", path=" + path + "].");
return image;
}
}
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);
}
/**
* Loads the configuration properties for our resource sets.
*/
protected Properties loadConfig (String configPath)
throws IOException
{
Properties config = new Properties();
try {
config.load(new FileInputStream(new File(_rdir, configPath)));
} catch (Exception e) {
String errmsg = "Unable to load resource manager config [rdir=" + _rdir +
", cpath=" + configPath + "]";
Log.warning(errmsg + ".");
Log.logStackTrace(e);
throw new IOException(errmsg);
}
return config;
}
/**
* If we have a full list of the resources available, we return it. A return value of null
* means that we do not know what's available and we'll have to try all possibilities. This
* is fine for most applications.
*/
protected HashSet