The illustrious beginnings of Getdown.

This commit is contained in:
Michael Bayne
2004-07-02 11:01:21 +00:00
parent 5881bad346
commit 2fb08f098a
10 changed files with 755 additions and 0 deletions
+77
View File
@@ -0,0 +1,77 @@
<!-- build configuration -->
<project name="getdown" default="compile" basedir=".">
<!-- various basic settings -->
<property name="app.name" value="getdown"/>
<property name="src.dir" value="src/java"/>
<property name="deploy.dir" value="dist"/>
<property name="dist.jar" value="${app.name}.jar"/>
<!-- we want to access the environment -->
<property environment="env"/>
<property name="java.libraries" value="${env.JAVA_LIBS}"/>
<!-- used when generating javadocs -->
<property name="javadoc.dir" value="${deploy.dir}/docs"/>
<property name="doc.packages" value="com.threerings.getdown.*"/>
<property name="doc.overview" value="com/threerings/getdown/overview.html"/>
<property name="copy.pre" value="Copyright &#169; 2004"/>
<property name="copyright.holder" value="Three Rings Design, Inc."/>
<property name="copy.post" value="All Rights Reserved."/>
<!-- declare our classpath -->
<path id="clazzpath">
<fileset dir="${java.libraries}" includesfile="lib/LIBS"/>
<fileset dir="lib" includes="**/*.jar"/>
<pathelement location="${deploy.dir}/classes"/>
</path>
<!-- prepares the application directories -->
<target name="prepare">
<mkdir dir="${deploy.dir}"/>
<mkdir dir="${deploy.dir}/classes"/>
<mkdir dir="${javadoc.dir}"/>
<!-- copy media and properties into the target directory -->
<copy todir="${deploy.dir}/classes">
<fileset dir="${src.dir}" includes="**/*.jpg"/>
<fileset dir="${src.dir}" includes="**/*.png"/>
<fileset dir="${src.dir}" includes="**/*.properties"/>
</copy>
</target>
<!-- cleans out the built application -->
<target name="clean">
<delete dir="${deploy.dir}"/>
</target>
<!-- build the java class files -->
<target name="compile" depends="prepare">
<javac srcdir="${src.dir}" destdir="${deploy.dir}/classes"
debug="on" optimize="off" deprecation="on"
classpathref="clazzpath" includeAntRuntime="no"/>
</target>
<!-- build the javadoc documentation -->
<target name="javadoc" depends="prepare">
<javadoc sourcepath="${src.dir}"
packagenames="${doc.packages}"
windowtitle="${app.name} API"
doctitle="${app.name} API"
overview="${src.dir}/${doc.overview}"
bottom="${copy.pre} ${copyright.holder} ${copy.post}"
destdir="${javadoc.dir}">
<classpath refid="clazzpath"/>
<link href="http://www.samskivert.com/code/samskivert/javadoc"/>
<link href="http://java.sun.com/products/jdk/1.4/docs/api"/>
</javadoc>
</target>
<!-- a target for rebuilding everything -->
<target name="all" depends="clean,prepare,compile,javadoc,dist"/>
<!-- builds our distribution files -->
<target name="dist" depends="prepare,compile">
<jar destfile="${deploy.dir}/${dist.jar}" manifest="lib/manifest.mf"
basedir="${deploy.dir}/classes"/>
</target>
</project>
+2
View File
@@ -0,0 +1,2 @@
samskivert.jar
ant.jar
+1
View File
@@ -0,0 +1 @@
Main-Class: com.threerings.getdown.launcher.Getdown
+38
View File
@@ -0,0 +1,38 @@
//
// $Id: Log.java,v 1.1 2004/07/02 11:01:21 mdb Exp $
package com.threerings.getdown;
/**
* A placeholder class that contains a reference to the log object used by
* the Getdown code.
*/
public class Log
{
public static com.samskivert.util.Log log =
new com.samskivert.util.Log("getdown");
/** Convenience function. */
public static void debug (String message)
{
log.debug(message);
}
/** Convenience function. */
public static void info (String message)
{
log.info(message);
}
/** Convenience function. */
public static void warning (String message)
{
log.warning(message);
}
/** Convenience function. */
public static void logStackTrace (Throwable t)
{
log.logStackTrace(com.samskivert.util.Log.WARNING, t);
}
}
@@ -0,0 +1,185 @@
//
// $Id: Application.java,v 1.1 2004/07/02 11:01:21 mdb Exp $
package com.threerings.getdown.data;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import com.samskivert.io.NestableIOException;
import com.samskivert.text.MessageUtil;
import com.samskivert.util.StringUtil;
import com.threerings.getdown.Log;
import com.threerings.getdown.util.ConfigUtil;
/**
* Parses and provide access to the information contained in the
* <code>getdown.txt</code> configuration file.
*/
public class Application
{
/** The name of our configuration file. */
public static final String CONFIG_FILE = "getdown.txt";
/**
* Creates an application instance which records the location of the
* <code>getdown.txt</code> configuration file from the supplied
* application directory.
*/
public Application (File appdir)
{
_appdir = appdir;
_config = new File(appdir, CONFIG_FILE);
}
/**
* Returns a resource that refers to the application configuration
* file itself.
*/
public Resource getConfigResource ()
{
try {
return new Resource(_appbase, _appdir, CONFIG_FILE);
} catch (Exception e) {
throw new RuntimeException("Booched appbase '" + _appbase + "'!?");
}
}
/**
* Returns a list of the code {@link Resource} objects used by this
* application.
*/
public List getCodeResources ()
{
return _codes;
}
/**
* Returns a list of the non-code {@link Resource} objects used by
* this application.
*/
public List getResources ()
{
return _resources;
}
/**
* Instructs the application to parse its <code>getdown.txt</code>
* configuration and prepare itself for operation. The application
* base URL will be parsed first so that if there are errors
* discovered later, the caller can use the application base to
* download a new <code>config.txt</code> file and try again.
*
* @exception IOException thrown if there is an error reading the file
* or an error encountered during its parsing.
*/
public void init ()
throws IOException
{
// parse our configuration file
HashMap cdata = ConfigUtil.parseConfig(_config);
// first determine our application base, this way if anything goes
// wrong later in the process, our caller can use the appbase to
// download a new configuration file
String appbase = (String)cdata.get("appbase");
if (appbase == null) {
throw new IOException("m.missing_appbase");
}
try {
// make sure there's a trailing slash
if (!appbase.endsWith("/")) {
appbase = appbase + "/";
}
_appbase = new URL(appbase);
} catch (Exception e) {
String err = MessageUtil.tcompose("m.invalid_appbase", appbase);
throw new NestableIOException(err, e);
}
// extract our version information
String vstr = (String)cdata.get("version");
if (vstr != null) {
try {
_version = Integer.parseInt(vstr);
} catch (Exception e) {
String err = MessageUtil.tcompose("m.invalid_version", vstr);
throw new NestableIOException(err, e);
}
}
// determine our application class name
_class = (String)cdata.get("class");
if (_class == null) {
throw new IOException("m.missing_class");
}
// parse our code resources
String[] codes = ConfigUtil.getMultiValue(cdata, "code");
if (codes == null) {
throw new IOException("m.missing_code");
}
for (int ii = 0; ii < codes.length; ii++) {
try {
_codes.add(new Resource(_appbase, _appdir, codes[ii]));
} catch (Exception e) {
Log.warning("Invalid code resource '" + codes[ii] + "'.");
}
}
// parse our non-code resources
String[] rsrcs = ConfigUtil.getMultiValue(cdata, "resource");
if (rsrcs != null) {
for (int ii = 0; ii < rsrcs.length; ii++) {
try {
_resources.add(new Resource(_appbase, _appdir, rsrcs[ii]));
} catch (Exception e) {
Log.warning("Invalid resource '" + rsrcs[ii] + "'.");
}
}
}
// transfer our JVM arguments
String[] jvmargs = ConfigUtil.getMultiValue(cdata, "jvmarg");
if (jvmargs != null) {
for (int ii = 0; ii < jvmargs.length; ii++) {
_jvmargs.add(jvmargs[ii]);
}
}
// transfer our application arguments
String[] appargs = ConfigUtil.getMultiValue(cdata, "apparg");
if (appargs != null) {
for (int ii = 0; ii < appargs.length; ii++) {
_appargs.add(appargs[ii]);
}
}
// Log.info("Parsed application " + _appbase);
// Log.info("Version: " + _version);
// Log.info("Class: " + _class);
// Log.info("Code: " + StringUtil.toString(_codes.iterator()));
// Log.info("Resources: " + StringUtil.toString(_resources.iterator()));
// Log.info("JVM Args: " + StringUtil.toString(_jvmargs.iterator()));
// Log.info("App Args: " + StringUtil.toString(_appargs.iterator()));
}
protected File _appdir;
protected File _config;
protected int _version = -1;
protected URL _appbase;
protected String _class;
protected ArrayList _codes = new ArrayList();
protected ArrayList _resources = new ArrayList();
protected ArrayList _jvmargs = new ArrayList();
protected ArrayList _appargs = new ArrayList();
}
@@ -0,0 +1,121 @@
//
// $Id: Digest.java,v 1.1 2004/07/02 11:01:21 mdb Exp $
package com.threerings.getdown.data;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import com.samskivert.text.MessageUtil;
import com.samskivert.util.StringUtil;
import com.threerings.getdown.util.ConfigUtil;
/**
* Manages the <code>digest.txt</code> file and the computing and
* processing of MD5 digests for an application.
*/
public class Digest
{
/** The name of our MD5 digest file. */
public static final String DIGEST_FILE = "digest.txt";
/**
* Creates a digest instance which will parse and validate the
* <code>digest.txt</code> in the supplied application directory.
*/
public Digest (File appdir)
throws IOException
{
// parse and validate our digest file contents
String metaDigest = "";
StringBuffer data = new StringBuffer();
List pairs = ConfigUtil.parsePairs(new File(appdir, DIGEST_FILE));
for (Iterator iter = pairs.iterator(); iter.hasNext(); ) {
String[] pair = (String[])iter.next();
if (pair[0].equals(DIGEST_FILE)) {
metaDigest = pair[1];
break;
}
_digests.put(pair[0], pair[1]);
note(data, pair[0], pair[1]);
}
// we've reached the end, validate our contents
MessageDigest md = getMessageDigest();
byte[] contents = data.toString().getBytes("UTF-8");
String md5 = StringUtil.hexlate(md.digest(contents));
if (!md5.equals(metaDigest)) {
String err = MessageUtil.tcompose(
"m.invalid_digest_file", metaDigest, md5);
throw new IOException(err);
}
}
/**
* Returns the stored digest value for the file with the supplied
* path, or null if no digest exists for a file with that path.
*/
public String getDigest (String path)
{
return (String)_digests.get(path);
}
/**
* Creates a digest file at the specified location using the supplied
* list of resources.
*/
public static void createDigest (List resources, File output)
throws IOException
{
MessageDigest md = getMessageDigest();
StringBuffer data = new StringBuffer();
PrintWriter pout = new PrintWriter(
new OutputStreamWriter(new FileOutputStream(output), "UTF-8"));
// compute and append the MD5 digest of each resource in the list
for (Iterator iter = resources.iterator(); iter.hasNext(); ) {
Resource rsrc = (Resource)iter.next();
String path = rsrc.getPath();
String digest = rsrc.computeDigest(md);
note(data, path, digest);
pout.println(path + " = " + digest);
}
// finally compute and append the digest for the file contents
md.reset();
byte[] contents = data.toString().getBytes("UTF-8");
pout.println(DIGEST_FILE + " = " +
StringUtil.hexlate(md.digest(contents)));
pout.close();
}
/** Used by {@link #createDigest} and {@link Digest}. */
protected static void note (StringBuffer data, String path, String digest)
{
data.append(path).append(" = ").append(digest).append("\n");
}
/** Used by {@link #createDigest} and {@link Digest}. */
protected static MessageDigest getMessageDigest ()
{
try {
return MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException nsae) {
throw new RuntimeException("JVM does not support MD5. Gurp!");
}
}
protected HashMap _digests = new HashMap();
}
@@ -0,0 +1,86 @@
//
// $Id: Resource.java,v 1.1 2004/07/02 11:01:21 mdb Exp $
package com.threerings.getdown.data;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.MessageDigest;
import com.samskivert.util.StringUtil;
/**
* Models a single file resource used by an {@link Application}.
*/
public class Resource
{
/**
* Creates a resource with the supplied application base URL and
* resource path.
*/
public Resource (URL appbase, File appdir, String path)
throws MalformedURLException
{
_path = path;
_remote = new URL(appbase, path);
_local = new File(appdir, path);
}
/**
* Returns the path associated with this resource.
*/
public String getPath ()
{
return _path;
}
/**
* Computes the MD5 hash of this resource's underlying file.
* <em>Note:</m> This is both CPU and I/O intensive.
*/
public String computeDigest (MessageDigest md)
throws IOException
{
md.reset();
byte[] buffer = new byte[DIGEST_BUFFER_SIZE];
int read;
FileInputStream fin = new FileInputStream(_local);
while ((read = fin.read(buffer)) != -1) {
md.update(buffer, 0, read);
}
return StringUtil.hexlate(md.digest());
}
/**
* Returns a string representation of this instance.
*/
public String toString ()
{
// return _path;
// return _netloc.toString();
return _local.getPath();
}
public static void main (String[] args)
{
try {
Resource rsrc = new Resource(
new URL("file:/"), new File("."), args[0]);
System.out.println("MD5: " + rsrc.computeDigest(
MessageDigest.getInstance("MD5")));
} catch (Exception e) {
e.printStackTrace(System.err);
}
}
protected String _path;
protected URL _remote;
protected File _local;
protected final static int DIGEST_BUFFER_SIZE = 5 * 1025;
}
@@ -0,0 +1,55 @@
//
// $Id: Getdown.java,v 1.1 2004/07/02 11:01:21 mdb Exp $
package com.threerings.getdown.launcher;
import java.io.File;
import com.threerings.getdown.Log;
import com.threerings.getdown.data.Application;
/**
* Manages the main control for the Getdown application updater and
* deployment system.
*/
public class Getdown
{
public Getdown (File appDir)
{
_app = new Application(appDir);
}
public void run ()
{
try {
_app.init();
} catch (Exception e) {
Log.logStackTrace(e);
}
}
public static void main (String[] args)
{
// ensure the proper parameters are passed
if (args.length != 1) {
System.err.println("Usage: java -jar getdown.jar app_dir");
System.exit(-1);
}
// ensure a valid directory was supplied
File appDir = new File(args[0]);
if (!appDir.exists() || !appDir.isDirectory()) {
Log.warning("Invalid app_dir '" + args[0] + "'.");
System.exit(-1);
}
try {
Getdown app = new Getdown(appDir);
app.run();
} catch (Exception e) {
Log.logStackTrace(e);
}
}
protected Application _app;
}
@@ -0,0 +1,71 @@
//
// $Id: DigesterTask.java,v 1.1 2004/07/02 11:01:21 mdb Exp $
package com.threerings.getdown.tools;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Task;
import com.samskivert.util.CollectionUtil;
import com.threerings.getdown.data.Application;
import com.threerings.getdown.data.Digest;
/**
* An ant task used to create a <code>digest.txt</code> for a Getdown
* application deployment.
*/
public class DigesterTask extends Task
{
/**
* Sets the application directory.
*/
public void setAppdir (File appdir)
{
_appdir = appdir;
}
/**
* Performs the actual work of the task.
*/
public void execute () throws BuildException
{
// make sure appdir is set
if (_appdir == null) {
String errmsg = "Must specify the path to the application " +
"directory via the 'appdir' attribute.";
throw new BuildException(errmsg);
}
File target = new File(_appdir, Digest.DIGEST_FILE);
System.out.println("Generating digest file '" + target + "'...");
// create our application and instruct it to parse its business
Application app = new Application(_appdir);
try {
app.init();
} catch (IOException ioe) {
throw new BuildException("Error parsing getdown.txt: " +
ioe.getMessage(), ioe);
}
ArrayList rsrcs = new ArrayList();
rsrcs.add(app.getConfigResource());
CollectionUtil.addAll(rsrcs, app.getCodeResources().iterator());
CollectionUtil.addAll(rsrcs, app.getResources().iterator());
// now generate the digest file
try {
Digest.createDigest(rsrcs, target);
} catch (IOException ioe) {
throw new BuildException("Error creating digest: " +
ioe.getMessage(), ioe);
}
}
/** The application directory in which we're creating a digest file. */
protected File _appdir;
}
@@ -0,0 +1,119 @@
//
// $Id: ConfigUtil.java,v 1.1 2004/07/02 11:01:21 mdb Exp $
package com.threerings.getdown.util;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import com.samskivert.util.StringUtil;
/**
* Parses a file containing key/value pairs and returns a {@link HashMap}
* with the values. Keys may be repeated, in which case they will be made
* to reference an array of values.
*/
public class ConfigUtil
{
/**
* Parses a configuration file containing key/value pairs. The file
* must be in the UTF-8 encoding.
*
* @return a list of <code>String[]</code> instances containing the
* key/value pairs in the order they were parsed from the file.
*/
public static List parsePairs (File config)
throws IOException
{
ArrayList pairs = new ArrayList();
// parse our configuration file
BufferedReader bin = new BufferedReader(
new InputStreamReader(new FileInputStream(config), "UTF-8"));
String line = null;
while ((line = bin.readLine()) != null) {
// nix comments
int cidx = line.indexOf("#");
if (cidx != -1) {
line = line.substring(0, cidx);
}
// trim whitespace and skip blank lines
line = line.trim();
if (StringUtil.blank(line)) {
continue;
}
// parse our key/value pair
String[] pair = new String[2];
int eidx = line.indexOf("=");
if (eidx != -1) {
pair[0] = line.substring(0, eidx).trim();
pair[1] = line.substring(eidx+1).trim();
} else {
pair[0] = line;
pair[1] = "";
}
pairs.add(pair);
}
return pairs;
}
/**
* Parses a configuration file containing key/value pairs. The file
* must be in the UTF-8 encoding.
*
* @return a map from keys to values, where a value will be an array
* of strings if more than one key/value pair in the config file was
* associated with the same key.
*/
public static HashMap parseConfig (File config)
throws IOException
{
List pairs = parsePairs(config);
HashMap data = new HashMap();
for (Iterator iter = pairs.iterator(); iter.hasNext(); ) {
String[] pair = (String[])iter.next();
Object value = data.get(pair[0]);
if (value == null) {
data.put(pair[0], pair[1]);
} else if (value instanceof String) {
data.put(pair[0], new String[] { (String)value, pair[1] });
} else if (value instanceof String[]) {
String[] values = (String[])value;
String[] nvalues = new String[values.length+1];
System.arraycopy(values, 0, nvalues, 0, values.length);
nvalues[values.length] = pair[1];
data.put(pair[0], nvalues);
}
}
return data;
}
/**
* Massages a single string into an array and leaves existing array
* values as is. Simplifies access to parameters that are expected to
* be arrays.
*/
public static String[] getMultiValue (HashMap data, String name)
{
Object value = data.get(name);
if (value instanceof String) {
return new String[] { (String)value };
} else {
return (String[])value;
}
}
}