diff --git a/bin/runjava b/bin/runjava
index ce1cb075f..fbed85a58 100755
--- a/bin/runjava
+++ b/bin/runjava
@@ -62,8 +62,8 @@ if (opendir(DIR, "$root/lib")) {
closedir DIR;
}
-# specify our server root (this is for server code)
-my $rootarg = "-Droot=$root";
+# specify our application root (the resource manager needs this)
+my $rootarg = "-Dapplication.root=$root";
my $pid_file = undef;
my $i = 0;
@@ -86,7 +86,7 @@ for ($i = 0; $i < @ARGV; $i++) {
$i -= 1; # decrement i so that things stay in sync
} elsif ($arg eq "-r") {
- $rootarg = "-Droot=" . $ARGV[$i+1];
+ $rootarg = "-DDapplication.root=" . $ARGV[$i+1];
splice(@ARGV, $i, 2);
$i -= 1; # decrement i so that things stay in sync
}
diff --git a/src/java/com/threerings/resource/ResourceBundle.java b/src/java/com/threerings/resource/ResourceBundle.java
new file mode 100644
index 000000000..c6f7cef39
--- /dev/null
+++ b/src/java/com/threerings/resource/ResourceBundle.java
@@ -0,0 +1,60 @@
+//
+// $Id: ResourceBundle.java,v 1.1 2001/11/20 00:21:41 mdb Exp $
+
+package com.threerings.resource;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+
+/**
+ * A resource bundle provides access to the resources in a jar file.
+ */
+public class ResourceBundle
+{
+ /**
+ * Constructs a resource bundle with the supplied jar file.
+ *
+ * @param source a file object that references our source jar file.
+ *
+ * @exception IOException thrown if an error occurs reading our jar
+ * file.
+ */
+ public ResourceBundle (File source)
+ throws IOException
+ {
+ _source = new JarFile(source);
+ }
+
+ /**
+ * Fetches the named resource from this bundle. The path should be
+ * specified as a relative, platform independent path (forward
+ * slashes). For example sounds/scream.au.
+ *
+ * @param path the path to the resource in this jar file.
+ *
+ * @return an input stream from which the resource can be loaded or
+ * null if no such resource exists.
+ *
+ * @exception IOException thrown if an error occurs locating the
+ * resource in the jar file.
+ */
+ public InputStream getResource (String path)
+ throws IOException
+ {
+ // TBD: determine whether or not we need to convert the path into
+ // a platform-dependent path if we're on Windows
+ JarEntry entry = _source.getJarEntry(path);
+ InputStream stream = null;
+ if (entry != null) {
+ stream = _source.getInputStream(entry);
+ }
+ return stream;
+ }
+
+ /** The jar file from which we load resources. */
+ protected JarFile _source;
+}
diff --git a/src/java/com/threerings/resource/ResourceManager.java b/src/java/com/threerings/resource/ResourceManager.java
index 8e54842cc..1c9ad5112 100644
--- a/src/java/com/threerings/resource/ResourceManager.java
+++ b/src/java/com/threerings/resource/ResourceManager.java
@@ -1,38 +1,234 @@
//
-// $Id: ResourceManager.java,v 1.2 2001/08/15 02:12:46 mdb Exp $
+// $Id: ResourceManager.java,v 1.3 2001/11/20 00:21:41 mdb Exp $
package com.threerings.resource;
-import java.io.*;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.io.InputStream;
+
import java.net.MalformedURLException;
import java.net.URL;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Properties;
+import java.util.StringTokenizer;
+
+import com.samskivert.Log;
+
/**
* 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 a set of jar files that contain resources
- * and that are updated from a remote resource repository via HTTP.
+ * 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). + * + *
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:
+ *
+ *
+ * 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 + *+ * + * 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.
*/
public class ResourceManager
{
/**
- * Temporary means by which to construct a resource manager that loads
- * resources from the specified source. The resource root is prepended
- * to any path that is requested and that fully qualified path is
- * searched for in the classpath.
+ * 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).
+ *
+ * @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 resourceRoot)
+ 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.
+ */
+ public ResourceManager (
+ String appRoot, String resourceRoot, Properties config)
{
// keep track of our root path
_rootPath = resourceRoot;
- // make root path end with a slash
+ // 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 + "/";
}
+ // get our app root if we weren't provided with one
+ if (appRoot == null) {
+ appRoot = System.getProperty(APP_ROOT_PROPERTY);
+ // 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));
+ }
+
+ } 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
+ 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(appRoot, setName, config.getProperty(key));
+ }
+ }
+
+ /**
+ * Loads up a resource set based on the supplied definition
+ * information.
+ */
+ protected void resolveResourceSet (
+ String appRoot, String name, String definition)
+ {
+ StringTokenizer tok =
+ new StringTokenizer(definition, File.pathSeparator);
+ ArrayList set = new ArrayList();
+
+ while (tok.hasMoreTokens()) {
+ // obtain the path and fully qualify it
+ String path = tok.nextToken();
+ if (!path.startsWith(File.separator)) {
+ path = appRoot + path;
+ }
+
+ try {
+ File efile = new File(path);
+
+ // if this isn't a directory, we assume it's a jar file
+ if (!efile.isDirectory()) {
+ set.add(new ResourceBundle(efile));
+ continue;
+ }
+
+ // 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");
+ }
+ });
+
+ if (efiles == null) {
+ Log.warning("Failure enumerating jars in directory " +
+ "[path=" + path + "].");
+ continue;
+ }
+
+ // phew, we made it
+ for (int i = 0; i < efiles.length; i++) {
+ set.add(new ResourceBundle(efiles[i]));
+ }
+
+ } catch (IOException ioe) {
+ Log.warning("Error processing resource set entry " +
+ "[entry=" + path + ", 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);
+
+ // if this is our default resource bundle, keep a reference to it
+ if (DEFAULT_RESOURCE_SET.equals(name)) {
+ _default = setvec;
+ }
}
/**
@@ -47,64 +243,53 @@ public class ResourceManager
public InputStream getResource (String path)
throws IOException
{
- String rpath = _rootPath + path;
- InputStream in = _loader.getResourceAsStream(rpath);
- if (in == null) {
- String errmsg = "Unable to locate resource [path=" + rpath + "]";
- throw new FileNotFoundException(errmsg);
- }
- return in;
- }
+ InputStream in = null;
- /**
- * Fetches the requested resource and loads its contents into a byte
- * array, which is returned. Note: this is hugely inefficient because
- * the data is copied twice while reading and then once entirely again
- * when a brand new byte array is returned for you, the caller (thanks
- * to the inflexibility of ByteArrayOutputStream). Anyone
- * reading a lot of resources should obtain an
- * InputStream and read the data directly into where it
- * needs to go.
- *
- * @exception IOException thrown if a problem occurs locating or
- * reading the resource.
- *
- * @see #getResource
- */
- public byte[] getResourceAsBytes (String path)
- throws IOException
- {
- InputStream in = getResource(path);
- ByteArrayOutputStream out = new ByteArrayOutputStream();
- byte[] buffer = new byte[512];
-
- // i loathe to while(1), but we need a non-trivial loop condition
- while (true) {
- int bytes = in.read(buffer, 0, buffer.length);
- if (bytes == 0) {
- throw new IOException("Read zero bytes!?");
- } else if (bytes < 0) {
- break;
+ // 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;
}
- out.write(buffer, 0, bytes);
}
- return out.toByteArray();
- }
-
- public static void main (String[] args)
- {
- try {
- ResourceManager rmgr = new ResourceManager("rsrc");
- byte[] data = rmgr.getResourceAsBytes(
- "config/miso/miso.properties");
- System.out.println(new String(data));
-
- } catch (Exception e) {
- e.printStackTrace(System.err);
+ // 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);
}
+ /** 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;
+
+ /** 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";
}