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"; }