From 49a7e2821606e2c445d4081dbb946e693f842505 Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Sun, 25 Sep 2016 10:28:50 -0700 Subject: [PATCH] Undid style changes, reduced OO boilerplate. --- .../classpath/CacheBasedClassPathBuilder.java | 61 ------- .../getdown/classpath/ClassPath.java | 38 ++-- .../getdown/classpath/ClassPathBuilder.java | 13 -- .../classpath/ClassPathBuilderBase.java | 14 -- .../classpath/ClassPathBuilderFactory.java | 22 --- .../getdown/classpath/ClassPathElement.java | 47 ----- .../getdown/classpath/ClassPaths.java | 76 ++++++++ .../classpath/DefaultClassPathBuilder.java | 30 ---- .../classpath/cache/GarbageCollector.java | 96 ++++------ .../threerings/getdown/data/Application.java | 164 +++++++----------- .../com/threerings/getdown/data/Digest.java | 21 +-- .../com/threerings/getdown/util/FileUtil.java | 47 +++-- .../threerings/getdown/util/LaunchUtil.java | 5 +- .../getdown/util/file/FileVisitor.java | 6 - .../getdown/util/file/FileWalker.java | 42 ----- .../CacheBasedClassPathBuilderTest.java | 44 ----- .../ClassPathBuilderFactoryTest.java | 37 ---- .../classpath/ClassPathBuilderTestBase.java | 48 ----- .../getdown/classpath/ClassPathTest.java | 37 ++-- .../getdown/classpath/ClassPathsTest.java | 68 ++++++++ .../DefaultClassPathBuilderTest.java | 31 ---- .../classpath/cache/GarbageCollectorTest.java | 49 ++---- .../classpath/cache/ResourceCacheTest.java | 48 ++--- .../threerings/getdown/util/FileUtilTest.java | 41 +++-- .../getdown/util/file/FileWalkerTest.java | 41 ----- 25 files changed, 373 insertions(+), 753 deletions(-) delete mode 100644 src/main/java/com/threerings/getdown/classpath/CacheBasedClassPathBuilder.java delete mode 100644 src/main/java/com/threerings/getdown/classpath/ClassPathBuilder.java delete mode 100644 src/main/java/com/threerings/getdown/classpath/ClassPathBuilderBase.java delete mode 100644 src/main/java/com/threerings/getdown/classpath/ClassPathBuilderFactory.java delete mode 100644 src/main/java/com/threerings/getdown/classpath/ClassPathElement.java create mode 100644 src/main/java/com/threerings/getdown/classpath/ClassPaths.java delete mode 100644 src/main/java/com/threerings/getdown/classpath/DefaultClassPathBuilder.java delete mode 100644 src/main/java/com/threerings/getdown/util/file/FileVisitor.java delete mode 100644 src/main/java/com/threerings/getdown/util/file/FileWalker.java delete mode 100644 src/test/java/com/threerings/getdown/classpath/CacheBasedClassPathBuilderTest.java delete mode 100644 src/test/java/com/threerings/getdown/classpath/ClassPathBuilderFactoryTest.java delete mode 100644 src/test/java/com/threerings/getdown/classpath/ClassPathBuilderTestBase.java create mode 100644 src/test/java/com/threerings/getdown/classpath/ClassPathsTest.java delete mode 100644 src/test/java/com/threerings/getdown/classpath/DefaultClassPathBuilderTest.java delete mode 100644 src/test/java/com/threerings/getdown/util/file/FileWalkerTest.java diff --git a/src/main/java/com/threerings/getdown/classpath/CacheBasedClassPathBuilder.java b/src/main/java/com/threerings/getdown/classpath/CacheBasedClassPathBuilder.java deleted file mode 100644 index e98b824..0000000 --- a/src/main/java/com/threerings/getdown/classpath/CacheBasedClassPathBuilder.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.threerings.getdown.classpath; - -import java.io.File; -import java.io.IOException; -import java.util.LinkedHashSet; -import java.util.concurrent.TimeUnit; - -import com.threerings.getdown.classpath.cache.GarbageCollector; -import com.threerings.getdown.classpath.cache.ResourceCache; -import com.threerings.getdown.data.Application; -import com.threerings.getdown.data.Resource; -import com.threerings.getdown.util.file.FileWalker; - -/** - * Instead of building the class path from the application directory, this class path builder puts - * the files that make up the class path into a cache and assembles the class path from the cache - * directory. - */ -public class CacheBasedClassPathBuilder extends ClassPathBuilderBase -{ - static final String CACHE_DIR = ".cache"; - - public CacheBasedClassPathBuilder(Application application) - { - super(application); - } - - @Override - public ClassPath buildClassPath () throws IOException - { - File cacheDir = new File(_application.getAppdir(), CACHE_DIR); - - // a negative value of code_cache_retention_days allows to clean up the cache forcefully - if (_application.getCodeCacheRetentionDays() <= 0) { - runGarbageCollection(cacheDir); - } - - ResourceCache cache = new ResourceCache(cacheDir); - - LinkedHashSet classPathEntries = new LinkedHashSet(); - - for (Resource resource: _application.getActiveCodeResources()) { - classPathEntries.add( - new ClassPathElement( - cache.cacheFile(resource.getFinalTarget(), _application.getDigest(resource)))); - } - - if (_application.getCodeCacheRetentionDays() > 0) { - runGarbageCollection(cacheDir); - } - - return new ClassPath(classPathEntries); - } - - private void runGarbageCollection(File cacheDir) { - GarbageCollector gc = new GarbageCollector( - new FileWalker(cacheDir), _application.getCodeCacheRetentionDays(), TimeUnit.DAYS); - - gc.collectGarbage(); - } -} diff --git a/src/main/java/com/threerings/getdown/classpath/ClassPath.java b/src/main/java/com/threerings/getdown/classpath/ClassPath.java index e5da5c6..d96b712 100644 --- a/src/main/java/com/threerings/getdown/classpath/ClassPath.java +++ b/src/main/java/com/threerings/getdown/classpath/ClassPath.java @@ -1,6 +1,7 @@ package com.threerings.getdown.classpath; import java.io.File; +import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.util.Collections; @@ -14,13 +15,13 @@ import java.util.Set; */ public class ClassPath { - public ClassPath (LinkedHashSet classPathEntries) + public ClassPath (LinkedHashSet classPathEntries) { - this._classPathEntries = Collections.unmodifiableSet(classPathEntries); + _classPathEntries = Collections.unmodifiableSet(classPathEntries); } /** - * Returns the class path as an java command line argument string, e.g + * Returns the class path as an java command line argument string, e.g. * *
      *   /path/to/a.jar:/path/to/b.jar
@@ -30,16 +31,10 @@ public class ClassPath
     {
         StringBuilder builder = new StringBuilder();
         String delimiter = "";
-
-        for (ClassPathElement entry: _classPathEntries)
-        {
-            builder
-                .append(delimiter)
-                .append(entry.getAbsolutePath());
-
+        for (File entry: _classPathEntries) {
+            builder.append(delimiter).append(entry.getAbsolutePath());
             delimiter = File.pathSeparator;
         }
-
         return builder.toString();
     }
 
@@ -50,20 +45,27 @@ public class ClassPath
     public URL[] asUrls ()
     {
         URL[] urls = new URL[_classPathEntries.size()];
-
         int i = 0;
-
-        for (ClassPathElement entry : _classPathEntries) {
-            urls[i++] = entry.getURL();
+        for (File entry : _classPathEntries) {
+            urls[i++] = getURL(entry);
         }
-
         return urls;
     }
 
-    public Set getClassPathEntries ()
+    public Set getClassPathEntries ()
     {
         return _classPathEntries;
     }
 
-    private final Set _classPathEntries;
+
+    private static URL getURL (File file)
+    {
+        try {
+            return file.toURI().toURL();
+        } catch (MalformedURLException e) {
+            throw new IllegalStateException("URL of file is illegal: " + file.getAbsolutePath(), e);
+        }
+    }
+
+    private final Set _classPathEntries;
 }
diff --git a/src/main/java/com/threerings/getdown/classpath/ClassPathBuilder.java b/src/main/java/com/threerings/getdown/classpath/ClassPathBuilder.java
deleted file mode 100644
index a220760..0000000
--- a/src/main/java/com/threerings/getdown/classpath/ClassPathBuilder.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.threerings.getdown.classpath;
-
-import java.io.IOException;
-
-/**
- * A class path builder compiles the class path of the application to be launched.
- */
-public interface ClassPathBuilder {
-    /**
-     * Builds and returns the class path of the application to be launched.
-     */
-    ClassPath buildClassPath () throws IOException;
-}
diff --git a/src/main/java/com/threerings/getdown/classpath/ClassPathBuilderBase.java b/src/main/java/com/threerings/getdown/classpath/ClassPathBuilderBase.java
deleted file mode 100644
index 0cc5613..0000000
--- a/src/main/java/com/threerings/getdown/classpath/ClassPathBuilderBase.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.threerings.getdown.classpath;
-
-import com.threerings.getdown.data.Application;
-
-/**
- * Base class for {@link ClassPathBuilder class path builders}.
- */
-abstract class ClassPathBuilderBase implements ClassPathBuilder {
-    public ClassPathBuilderBase(Application _application) {
-        this._application = _application;
-    }
-
-    protected final Application _application;
-}
diff --git a/src/main/java/com/threerings/getdown/classpath/ClassPathBuilderFactory.java b/src/main/java/com/threerings/getdown/classpath/ClassPathBuilderFactory.java
deleted file mode 100644
index 5f93fdb..0000000
--- a/src/main/java/com/threerings/getdown/classpath/ClassPathBuilderFactory.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package com.threerings.getdown.classpath;
-
-import com.threerings.getdown.data.Application;
-
-/**
- * A factory for {@link ClassPathBuilder class path builders}.
- */
-public abstract class ClassPathBuilderFactory {
-    private ClassPathBuilderFactory () {
-    }
-
-    /**
-     * Creates a class path builder that is able to compile the class path of the supplied
-     * {@link Application application}.
-     */
-    public static ClassPathBuilder create (Application application)
-    {
-        return application.isUseCodeCache()
-                ? new CacheBasedClassPathBuilder(application)
-                        : new DefaultClassPathBuilder(application);
-    }
-}
diff --git a/src/main/java/com/threerings/getdown/classpath/ClassPathElement.java b/src/main/java/com/threerings/getdown/classpath/ClassPathElement.java
deleted file mode 100644
index edf64a2..0000000
--- a/src/main/java/com/threerings/getdown/classpath/ClassPathElement.java
+++ /dev/null
@@ -1,47 +0,0 @@
-package com.threerings.getdown.classpath;
-
-import java.io.File;
-import java.net.MalformedURLException;
-import java.net.URL;
-
-/**
- * Represents an element of an applications class path.
- */
-public class ClassPathElement
-{
-    public ClassPathElement (File _file)
-    {
-        this._file = _file;
-    }
-
-    /**
-     * Returns the code resource.
-     */
-    public File getFile ()
-    {
-        return _file;
-    }
-
-
-    /**
-     * Returns the elements absolute path on the file system.
-     */
-    public String getAbsolutePath ()
-    {
-        return _file.getAbsolutePath();
-    }
-
-    /**
-     * Returns the file URL of this class path element.
-     */
-    public URL getURL ()
-    {
-        try {
-            return _file.toURI().toURL();
-        } catch (MalformedURLException e) {
-            throw new IllegalStateException("URL of file is illegal: " + getAbsolutePath(), e);
-        }
-    }
-
-    private final File _file;
-}
diff --git a/src/main/java/com/threerings/getdown/classpath/ClassPaths.java b/src/main/java/com/threerings/getdown/classpath/ClassPaths.java
new file mode 100644
index 0000000..84b5607
--- /dev/null
+++ b/src/main/java/com/threerings/getdown/classpath/ClassPaths.java
@@ -0,0 +1,76 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2016 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.classpath;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.LinkedHashSet;
+import java.util.concurrent.TimeUnit;
+
+import com.threerings.getdown.classpath.cache.GarbageCollector;
+import com.threerings.getdown.classpath.cache.ResourceCache;
+import com.threerings.getdown.data.Application;
+import com.threerings.getdown.data.Resource;
+
+public class ClassPaths
+{
+    static final String CACHE_DIR = ".cache";
+
+    /**
+     * Builds either a default or cached classpath based on {@code app}'s configuration.
+     */
+    public static ClassPath buildClassPath (Application app) throws IOException
+    {
+        return app.useCodeCache() ? buildDefaultClassPath(app) : buildCachedClassPath(app);
+    }
+
+    /**
+     * Builds a {@link ClassPath} instance for {@code app} using the code resources in place in
+     * the app directory.
+     */
+    public static ClassPath buildDefaultClassPath (Application app)
+    {
+        LinkedHashSet classPathEntries = new LinkedHashSet();
+        for (Resource resource: app.getActiveCodeResources()) {
+            classPathEntries.add(resource.getFinalTarget());
+        }
+        return new ClassPath(classPathEntries);
+    }
+
+    /**
+     * Builds a {@link ClassPath} instance for {@code app} by first copying the code resources into
+     * a cache directory and then referencing them from there. This avoids problems with
+     * overwriting in-use classpath elements when the application is later updated. This also
+     * "garbage collects" expired caches if necessary.
+     */
+    public static ClassPath buildCachedClassPath (Application app) throws IOException
+    {
+        File cacheDir = new File(app.getAppdir(), CACHE_DIR);
+        // a negative value of code_cache_retention_days allows to clean up the cache forcefully
+        if (app.getCodeCacheRetentionDays() <= 0) {
+            runGarbageCollection(app, cacheDir);
+        }
+
+        ResourceCache cache = new ResourceCache(cacheDir);
+        LinkedHashSet classPathEntries = new LinkedHashSet();
+        for (Resource resource: app.getActiveCodeResources()) {
+            File entry = cache.cacheFile(resource.getFinalTarget(), app.getDigest(resource));
+            classPathEntries.add(entry);
+        }
+
+        if (app.getCodeCacheRetentionDays() > 0) {
+            runGarbageCollection(app, cacheDir);
+        }
+
+        return new ClassPath(classPathEntries);
+    }
+
+    private static void runGarbageCollection (Application app, File cacheDir)
+    {
+        long retainMillis = TimeUnit.DAYS.toMillis(app.getCodeCacheRetentionDays());
+        GarbageCollector.collect(cacheDir, retainMillis);
+    }
+}
diff --git a/src/main/java/com/threerings/getdown/classpath/DefaultClassPathBuilder.java b/src/main/java/com/threerings/getdown/classpath/DefaultClassPathBuilder.java
deleted file mode 100644
index b6f375b..0000000
--- a/src/main/java/com/threerings/getdown/classpath/DefaultClassPathBuilder.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.threerings.getdown.classpath;
-
-import java.util.LinkedHashSet;
-
-import com.threerings.getdown.data.Application;
-import com.threerings.getdown.data.Resource;
-
-/**
- * The default class path builder assembles the class path from the code resources to be found in
- * the application directory.
- */
-public class DefaultClassPathBuilder extends ClassPathBuilderBase
-{
-    public DefaultClassPathBuilder(Application _application) {
-        super(_application);
-    }
-
-    @Override
-    public ClassPath buildClassPath ()
-    {
-        LinkedHashSet classPathEntries = new LinkedHashSet();
-
-        for (Resource resource: _application.getActiveCodeResources()) {
-            classPathEntries.add(new ClassPathElement(resource.getFinalTarget()));
-        }
-
-        return new ClassPath(classPathEntries);
-    }
-
-}
diff --git a/src/main/java/com/threerings/getdown/classpath/cache/GarbageCollector.java b/src/main/java/com/threerings/getdown/classpath/cache/GarbageCollector.java
index 6a40ae1..6c1dbc7 100644
--- a/src/main/java/com/threerings/getdown/classpath/cache/GarbageCollector.java
+++ b/src/main/java/com/threerings/getdown/classpath/cache/GarbageCollector.java
@@ -1,10 +1,7 @@
 package com.threerings.getdown.classpath.cache;
 
 import java.io.File;
-import java.util.concurrent.TimeUnit;
-
-import com.threerings.getdown.util.file.FileVisitor;
-import com.threerings.getdown.util.file.FileWalker;
+import com.threerings.getdown.util.FileUtil;
 
 /**
  * Collects elements in the {@link ResourceCache cache} which became unused and deletes them
@@ -12,80 +9,53 @@ import com.threerings.getdown.util.file.FileWalker;
  */
 public class GarbageCollector
 {
-    public GarbageCollector (
-            FileWalker _fileWalker, int retentionPeriod, TimeUnit retentionPeriodTimeUnit)
-    {
-        this._fileWalker = _fileWalker;
-        this._retentionMillis = retentionPeriodTimeUnit.toMillis(retentionPeriod);
-    }
-
     /**
      * Collect and delete the garbage in the cache.
      */
-    public void collectGarbage ()
+    public static void collect (File cacheDir, final long retentionPeriodMillis)
     {
-        _fileWalker.walkTree(new FileVisitor() {
-            @Override
-            public void visit(File file)
-            {
-                collect(file);
+        FileUtil.walkTree(cacheDir, new FileUtil.Visitor() {
+            @Override public void visit (File file) {
+                File cachedFile = getCachedFile(file);
+                File lastAccessedFile = getLastAccessedFile(file);
+                if (!cachedFile.exists() || !lastAccessedFile.exists()) {
+                    if (cachedFile.exists()) {
+                        cachedFile.delete();
+                    } else {
+                        lastAccessedFile.delete();
+                    }
+                } else if (shouldDelete(lastAccessedFile, retentionPeriodMillis)) {
+                    lastAccessedFile.delete();
+                    cachedFile.delete();
+                }
+
+                File folder = file.getParentFile();
+                if (folder.list().length == 0) {
+                    folder.delete();
+                }
             }
         });
     }
 
-    private void collect (File file) {
-        File cachedFile = getCachedFile(file);
-        File lastAccessedFile = getLastAccessedFile(file);
-
-        if (!cachedFile.exists() || !lastAccessedFile.exists()) {
-            if (cachedFile.exists()) {
-                cachedFile.delete();
-            } else {
-                lastAccessedFile.delete();
-            }
-        } else if (shouldDelete(lastAccessedFile)) {
-            lastAccessedFile.delete();
-            cachedFile.delete();
-        }
-
-        File folder = file.getParentFile();
-
-        if (folder.list().length == 0) {
-            folder.delete();
-        }
-    }
-
-    private boolean shouldDelete(File lastAccessedFile) {
-        return System.currentTimeMillis() - lastAccessedFile.lastModified() > _retentionMillis;
-    }
-
-    private File getLastAccessedFile (File file)
+    private static boolean shouldDelete (File lastAccessedFile, long retentionMillis)
     {
-        if (isLastAccessedFile(file)) {
-            return file;
-        }
-
-        return new File(
-                file.getParentFile(),
-                file.getName() + ResourceCache.LAST_ACCESSED_FILE_SUFFIX);
+        return System.currentTimeMillis() - lastAccessedFile.lastModified() > retentionMillis;
     }
 
-    private boolean isLastAccessedFile (File file)
+    private static File getLastAccessedFile (File file)
+    {
+        return isLastAccessedFile(file) ? file : new File(
+            file.getParentFile(), file.getName() + ResourceCache.LAST_ACCESSED_FILE_SUFFIX);
+    }
+
+    private static boolean isLastAccessedFile (File file)
     {
         return file.getName().endsWith(ResourceCache.LAST_ACCESSED_FILE_SUFFIX);
     }
 
-    private File getCachedFile (File file)
+    private static File getCachedFile (File file)
     {
-        if (!isLastAccessedFile(file)) {
-            return file;
-        }
-
-        return new File(
-                file.getParentFile(),
-                file.getName().substring(0, file.getName().lastIndexOf(".")));
+        return !isLastAccessedFile(file) ? file : new File(
+            file.getParentFile(), file.getName().substring(0, file.getName().lastIndexOf(".")));
     }
-
-    private final FileWalker _fileWalker;
-    private final long _retentionMillis;
 }
diff --git a/src/main/java/com/threerings/getdown/data/Application.java b/src/main/java/com/threerings/getdown/data/Application.java
index 4590f64..ce72f50 100644
--- a/src/main/java/com/threerings/getdown/data/Application.java
+++ b/src/main/java/com/threerings/getdown/data/Application.java
@@ -5,21 +5,9 @@
 
 package com.threerings.getdown.data;
 
-import static com.threerings.getdown.Log.log;
-
 import java.awt.Color;
 import java.awt.Rectangle;
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.FileReader;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.PrintStream;
-import java.io.RandomAccessFile;
+import java.io.*;
 import java.lang.reflect.Method;
 import java.net.MalformedURLException;
 import java.net.URL;
@@ -28,46 +16,29 @@ import java.net.URLConnection;
 import java.nio.channels.FileChannel;
 import java.nio.channels.FileLock;
 import java.nio.channels.OverlappingFileLockException;
-import java.security.AllPermission;
-import java.security.CodeSource;
-import java.security.GeneralSecurityException;
-import java.security.PermissionCollection;
-import java.security.Permissions;
-import java.security.Signature;
+import java.security.*;
 import java.security.cert.Certificate;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.EnumMap;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import javax.swing.JApplet;
 
-import org.apache.commons.codec.binary.Base64;
-
 import com.samskivert.io.StreamUtil;
 import com.samskivert.text.MessageUtil;
 import com.samskivert.util.ArrayUtil;
 import com.samskivert.util.RandomUtil;
 import com.samskivert.util.RunAnywhere;
 import com.samskivert.util.StringUtil;
+
+import org.apache.commons.codec.binary.Base64;
+
+import com.threerings.getdown.classpath.ClassPaths;
 import com.threerings.getdown.classpath.ClassPath;
-import com.threerings.getdown.classpath.ClassPathBuilderFactory;
 import com.threerings.getdown.launcher.RotatingBackgrounds;
-import com.threerings.getdown.util.ConfigUtil;
-import com.threerings.getdown.util.ConnectionUtil;
-import com.threerings.getdown.util.FileUtil;
-import com.threerings.getdown.util.LaunchUtil;
-import com.threerings.getdown.util.ProgressAggregator;
-import com.threerings.getdown.util.ProgressObserver;
-import com.threerings.getdown.util.VersionUtil;
+import com.threerings.getdown.util.*;
+
+import static com.threerings.getdown.Log.log;
 
 /**
  * Parses and provide access to the information contained in the getdown.txt
@@ -257,10 +228,36 @@ public class Application
     {
         _appdir = appdir;
         _appid = appid;
-        _signers = signers == null ? Collections.emptyList() : signers;
+        _signers = (signers == null) ? Collections.emptyList() : signers;
         _config = getLocalPath(CONFIG_FILE);
-        _extraJvmArgs = jvmargs == null ? ArrayUtil.EMPTY_STRING : jvmargs;
-        _extraAppArgs = appargs == null ? ArrayUtil.EMPTY_STRING : appargs;
+        _extraJvmArgs = (jvmargs == null) ? ArrayUtil.EMPTY_STRING : jvmargs;
+        _extraAppArgs = (appargs == null) ? ArrayUtil.EMPTY_STRING : appargs;
+    }
+
+    /**
+     * Returns the configured application directory.
+     */
+    public File getAppdir()
+    {
+        return _appdir;
+    }
+
+    /**
+     * Returns whether the application should cache code resources prior to launching the
+     * application.
+     */
+    public boolean useCodeCache ()
+    {
+        return _useCodeCache;
+    }
+
+    /**
+     * Returns the number of days a cached code resource is allowed to stay unused before it
+     * becomes eligible for deletion.
+     */
+    public int getCodeCacheRetentionDays ()
+    {
+        return _codeCacheRetentionDays;
     }
 
     /**
@@ -291,6 +288,14 @@ public class Application
         return _resources;
     }
 
+    /**
+     * Returns the digest of the given {@code resource}.
+     */
+    public String getDigest (Resource resource)
+    {
+        return _digest.getDigest(resource);
+    }
+
     /**
      * Returns a list of all the active {@link Resource} objects used by this application (code and
      * non-code).
@@ -383,7 +388,7 @@ public class Application
             return null;
         }
 
-        String infix = auxgroup == null ? "" : "-" + auxgroup;
+        String infix = (auxgroup == null) ? "" : ("-" + auxgroup);
         String pfile = "patch" + infix + _version + ".dat";
         try {
             URL remote = new URL(createVAppBase(_targetVersion), encodePath(pfile));
@@ -545,7 +550,7 @@ public class Application
 
         // if we are a versioned deployment, create a versioned appbase
         try {
-            _vappbase = _version < 0 ? new URL(_appbase) : createVAppBase(_version);
+            _vappbase = (_version < 0) ? new URL(_appbase) : createVAppBase(_version);
         } catch (MalformedURLException mue) {
             String err = MessageUtil.tcompose("m.invalid_appbase", _appbase);
             throw (IOException) new IOException(err).initCause(mue);
@@ -562,7 +567,7 @@ public class Application
             }
         }
 
-        String appPrefix = StringUtil.isBlank(_appid) ? "" : _appid + ".";
+        String appPrefix = StringUtil.isBlank(_appid) ? "" : (_appid + ".");
 
         // determine our application class name
         _class = (String)cdata.get(appPrefix + "class");
@@ -689,8 +694,8 @@ public class Application
 
         // whether to cache code resources and launch from cache
         _useCodeCache = Boolean.parseBoolean((String) cdata.get("use_code_cache"));
-        _codeCacheRetentionDays = cdata.containsKey("code_cache_retention_days")
-                ? Integer.parseInt((String) cdata.get("use_code_cache")) : 7;
+        _codeCacheRetentionDays = cdata.containsKey("code_cache_retention_days") ?
+            Integer.parseInt((String) cdata.get("use_code_cache")) : 7;
 
         // parse and return our application config
         UpdateInterface ui = new UpdateInterface();
@@ -709,9 +714,9 @@ public class Application
         }
         // and now ui.background can refer to the background color, but fall back to black
         // or white, depending on the brightness of the progressText
-        Color defaultBackground = .5f < Color.RGBtoHSB(
+        Color defaultBackground = (.5f < Color.RGBtoHSB(
                 ui.progressText.getRed(), ui.progressText.getGreen(), ui.progressText.getBlue(),
-                null)[2]
+                null)[2])
             ? Color.BLACK
             : Color.WHITE;
         ui.background = parseColor(cdata, "ui.background", defaultBackground);
@@ -726,7 +731,7 @@ public class Application
 
         // On an installation error, where do we point the user.
         String installError = parseUrl(cdata, "ui.install_error", null);
-        ui.installError = installError == null ?
+        ui.installError = (installError == null) ?
             "m.default_install_error" : MessageUtil.taint(installError);
 
         // the patch notes bits
@@ -838,8 +843,8 @@ public class Application
                 }
             }
 
-            boolean minVersionOK = _javaMinVersion == 0 || version >= _javaMinVersion;
-            boolean maxVersionOK = _javaMaxVersion == 0 || version <= _javaMaxVersion;
+            boolean minVersionOK = (_javaMinVersion == 0) || (version >= _javaMinVersion);
+            boolean maxVersionOK = (_javaMaxVersion == 0) || (version <= _javaMaxVersion);
             return minVersionOK && maxVersionOK;
 
         } catch (RuntimeException re) {
@@ -937,7 +942,7 @@ public class Application
         boolean dashJarMode = MANIFEST_CLASS.equals(_class);
 
         // add the -classpath arguments if we're not in -jar mode
-        ClassPath classPath = ClassPathBuilderFactory.create(this).buildClassPath();
+        ClassPath classPath = ClassPaths.buildClassPath(this);
 
         if (!dashJarMode) {
             args.add("-classpath");
@@ -1037,18 +1042,14 @@ public class Application
 
     /**
      * Runs this application directly in the current VM.
-     * @throws IOException
      */
     public void invokeDirect (JApplet applet) throws IOException
     {
-        ClassPath classPath = ClassPathBuilderFactory.create(this).buildClassPath();
-
+        ClassPath classPath = ClassPaths.buildClassPath(this);
         URL[] jarUrls = classPath.asUrls();
 
         // create custom class loader
-        URLClassLoader loader = new URLClassLoader(
-            jarUrls,
-            ClassLoader.getSystemClassLoader()) {
+        URLClassLoader loader = new URLClassLoader(jarUrls, ClassLoader.getSystemClassLoader()) {
             @Override protected PermissionCollection getPermissions (CodeSource code) {
                 Permissions perms = new Permissions();
                 perms.add(new AllPermission());
@@ -1058,9 +1059,7 @@ public class Application
         Thread.currentThread().setContextClassLoader(loader);
 
         log.info("Configured URL class loader:");
-        for (URL url : jarUrls) {
-            log.info("  " + url);
-        }
+        for (URL url : jarUrls) log.info("  " + url);
 
         // configure any system properties that we can
         for (String jvmarg : _jvmargs) {
@@ -1169,7 +1168,7 @@ public class Application
         if (_version == -1) {
             // make a note of the old meta-digest, if this changes we need to revalidate all of our
             // resources as one or more of them have also changed
-            String olddig = _digest == null ? "" : _digest.getMetaDigest();
+            String olddig = (_digest == null) ? "" : _digest.getMetaDigest();
             try {
                 status.updateStatus("m.checking");
                 downloadDigestFile();
@@ -1323,7 +1322,7 @@ public class Application
             failures.add(rsrc);
         }
 
-        return failures.size() == 0 ? null : failures;
+        return (failures.size() == 0) ? null : failures;
     }
 
     /**
@@ -1381,24 +1380,6 @@ public class Application
         return _version;
     }
 
-    /**
-     * Returns whether the application should cache code resources prior to launching the
-     * application.
-     */
-    public boolean isUseCodeCache ()
-    {
-        return _useCodeCache;
-    }
-
-    /**
-     * Returns the number of days a cached code resource is allowed to stay unused before it
-     * becomes eligible for deletion.
-     */
-    public int getCodeCacheRetentionDays ()
-    {
-        return _codeCacheRetentionDays;
-    }
-
     /**
      * Creates a versioned application base URL for the specified version.
      */
@@ -1644,7 +1625,7 @@ public class Application
     {
         String value = (String)cdata.get(name);
         Rectangle rect = parseRect(name, value);
-        return rect == null ? def : rect;
+        return (rect == null) ? def : rect;
     }
 
     /** Helper function to add all values in {@code values} (if non-null) to {@code target}. */
@@ -1689,7 +1670,7 @@ public class Application
     {
         String value = (String)cdata.get(name);
         Color color = parseColor(value);
-        return color == null ? def : color;
+        return (color == null) ? def : color;
     }
 
     /**
@@ -1712,7 +1693,7 @@ public class Application
     protected String[] parseList (Map cdata, String name)
     {
         String value = (String)cdata.get(name);
-        return value == null ? ArrayUtil.EMPTY_STRING : StringUtil.parseStringArray(value);
+        return (value == null) ? ArrayUtil.EMPTY_STRING : StringUtil.parseStringArray(value);
     }
 
     /**
@@ -1788,19 +1769,6 @@ public class Application
         }
     }
 
-    /**
-     * Returns the digest of the given {@code resource}.
-     */
-    public String getDigest (Resource resource)
-    {
-        return _digest.getDigest(resource);
-    }
-
-    public File getAppdir()
-    {
-        return _appdir;
-    }
-
     protected File _appdir;
     protected String _appid;
     protected File _config;
diff --git a/src/main/java/com/threerings/getdown/data/Digest.java b/src/main/java/com/threerings/getdown/data/Digest.java
index 5cdddf2..9d30c6f 100644
--- a/src/main/java/com/threerings/getdown/data/Digest.java
+++ b/src/main/java/com/threerings/getdown/data/Digest.java
@@ -5,8 +5,6 @@
 
 package com.threerings.getdown.data;
 
-import static com.threerings.getdown.Log.log;
-
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
@@ -20,9 +18,12 @@ import java.util.List;
 import com.samskivert.io.StreamUtil;
 import com.samskivert.text.MessageUtil;
 import com.samskivert.util.StringUtil;
+
 import com.threerings.getdown.util.ConfigUtil;
 import com.threerings.getdown.util.ProgressObserver;
 
+import static com.threerings.getdown.Log.log;
+
 /**
  * Manages the digest.txt file and the computing and processing of MD5 digests for an
  * application.
@@ -92,6 +93,14 @@ public class Digest
         return false;
     }
 
+    /**
+     * Returns the digest of the given {@code resource}.
+     */
+    public String getDigest (Resource resource)
+    {
+        return _digests.get(resource.getPath());
+    }
+
     /**
      * Creates a digest file at the specified location using the supplied list of resources.
      */
@@ -145,14 +154,6 @@ public class Digest
         data.append(path).append(" = ").append(digest).append("\n");
     }
 
-    /**
-     * Returns the digest of the given {@code resource}.
-     */
-    public String getDigest (Resource resource)
-    {
-        return _digests.get(resource.getPath());
-    }
-
     protected HashMap _digests = new HashMap();
     protected String _metaDigest = "";
 }
diff --git a/src/main/java/com/threerings/getdown/util/FileUtil.java b/src/main/java/com/threerings/getdown/util/FileUtil.java
index bbfcb2a..f8362eb 100644
--- a/src/main/java/com/threerings/getdown/util/FileUtil.java
+++ b/src/main/java/com/threerings/getdown/util/FileUtil.java
@@ -5,16 +5,11 @@
 
 package com.threerings.getdown.util;
 
-import static com.threerings.getdown.Log.log;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.Reader;
+import java.io.*;
+import java.util.ArrayDeque;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Deque;
 import java.util.List;
 import java.util.jar.JarOutputStream;
 import java.util.jar.Pack200;
@@ -22,6 +17,8 @@ import java.util.zip.GZIPInputStream;
 
 import com.samskivert.io.StreamUtil;
 
+import static com.threerings.getdown.Log.log;
+
 /**
  * File related utilities.
  */
@@ -115,6 +112,7 @@ public class FileUtil extends com.samskivert.util.FileUtil
             Pack200.Unpacker unpacker = Pack200.newUnpacker();
             unpacker.unpack(packedJarIn, jarOutputStream);
             return true;
+
         } catch (IOException e) {
             log.warning("Failed to unpack packed 200 jar file", "jar", packedJar, "error", e);
             return false;
@@ -129,18 +127,43 @@ public class FileUtil extends com.samskivert.util.FileUtil
     /**
      * Copies the given {@code source} file to the given {@code target}.
      */
-    public static void copy(File source, File target) throws IOException {
+    public static void copy (File source, File target) throws IOException {
         FileInputStream in = null;
         FileOutputStream out = null;
-
         try {
             in = new FileInputStream(source);
             out = new FileOutputStream(target);
-
             StreamUtil.copy(in, out);
         } finally {
             StreamUtil.close(in);
             StreamUtil.close(out);
         }
     }
+
+    /**
+     * Used by {@link #walkTree}.
+     */
+    public interface Visitor
+    {
+        void visit (File file);
+    }
+
+    /**
+     * Walks all files in {@code root}, calling {@code visitor} on each file in the tree.
+     */
+    public static void walkTree (File root, Visitor visitor)
+    {
+        Deque stack = new ArrayDeque(Arrays.asList(root.listFiles()));
+        while (!stack.isEmpty()) {
+            File currentFile = stack.pop();
+            if (currentFile.exists()) {
+                visitor.visit(currentFile);
+                if (currentFile.isDirectory()) {
+                    for (File file: currentFile.listFiles()) {
+                        stack.push(file);
+                    }
+                }
+            }
+        }
+    }
 }
diff --git a/src/main/java/com/threerings/getdown/util/LaunchUtil.java b/src/main/java/com/threerings/getdown/util/LaunchUtil.java
index 1aeabdf..716961d 100644
--- a/src/main/java/com/threerings/getdown/util/LaunchUtil.java
+++ b/src/main/java/com/threerings/getdown/util/LaunchUtil.java
@@ -10,7 +10,6 @@ import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.PrintStream;
-import com.samskivert.io.StreamUtil;
 import com.samskivert.util.RunAnywhere;
 import com.samskivert.util.StringUtil;
 
@@ -155,7 +154,7 @@ public class LaunchUtil
                 try {
                     // copy the moved file back to getdown-dop-new.jar so that we don't end up
                     // downloading another copy next time
-                    StreamUtil.copy(new FileInputStream(curgd), new FileOutputStream(newgd));
+                    FileUtil.copy(curgd, newgd);
                 } catch (IOException e) {
                     log.warning("Error copying updated Getdown back: " + e);
                 }
@@ -172,7 +171,7 @@ public class LaunchUtil
         // that didn't work, let's try copying it
         log.info("Attempting to upgrade by copying over " + curgd + "...");
         try {
-            StreamUtil.copy(new FileInputStream(newgd), new FileOutputStream(curgd));
+            FileUtil.copy(newgd, curgd);
         } catch (IOException ioe) {
             log.warning("Mayday! Brute force copy method also failed.", ioe);
         }
diff --git a/src/main/java/com/threerings/getdown/util/file/FileVisitor.java b/src/main/java/com/threerings/getdown/util/file/FileVisitor.java
deleted file mode 100644
index 29f01ed..0000000
--- a/src/main/java/com/threerings/getdown/util/file/FileVisitor.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package com.threerings.getdown.util.file;
-import java.io.File;
-
-public interface FileVisitor {
-    void visit(File file);
-}
diff --git a/src/main/java/com/threerings/getdown/util/file/FileWalker.java b/src/main/java/com/threerings/getdown/util/file/FileWalker.java
deleted file mode 100644
index 2ffc613..0000000
--- a/src/main/java/com/threerings/getdown/util/file/FileWalker.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package com.threerings.getdown.util.file;
-
-import java.io.File;
-import java.util.ArrayDeque;
-import java.util.Arrays;
-import java.util.Deque;
-
-/**
- * Recursively walks over files and folders. A {@link FileVisitor visitor} is informed for each path
- * traversed.
- */
-public class FileWalker
-{
-    public FileWalker (File start)
-    {
-        this._start = start;
-    }
-
-    /**
-     * Walks over the directory tree with the provided {@code visitor}.
-     */
-    public void walkTree (FileVisitor visitor)
-    {
-        Deque stack = new ArrayDeque(Arrays.asList(_start.listFiles()));
-
-        while (!stack.isEmpty()) {
-            File currentFile = stack.pop();
-
-            if (currentFile.exists()) {
-                visitor.visit(currentFile);
-
-                if (currentFile.isDirectory()) {
-                    for (File file: currentFile.listFiles()) {
-                        stack.push(file);
-                    }
-                }
-            }
-        }
-    }
-
-    private final File _start;
-}
diff --git a/src/test/java/com/threerings/getdown/classpath/CacheBasedClassPathBuilderTest.java b/src/test/java/com/threerings/getdown/classpath/CacheBasedClassPathBuilderTest.java
deleted file mode 100644
index f76a317..0000000
--- a/src/test/java/com/threerings/getdown/classpath/CacheBasedClassPathBuilderTest.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package com.threerings.getdown.classpath;
-
-import static org.junit.Assert.assertEquals;
-import static org.mockito.Mockito.when;
-
-import java.io.File;
-import java.io.IOException;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.runners.MockitoJUnitRunner;
-
-import com.threerings.getdown.util.FileUtil;
-
-/**
- * Tests {@link CacheBasedClassPathBuilderTest}.
- */
-@RunWith(MockitoJUnitRunner.class)
-public class CacheBasedClassPathBuilderTest extends ClassPathBuilderTestBase
-{
-    @Test
-    public void shouldBuildCacheBasedClassPath () throws IOException
-    {
-        when(_application.getDigest(_firstJar)).thenReturn("first");
-        when(_application.getDigest(_secondJar)).thenReturn("second");
-        when(_application.getCodeCacheRetentionDays()).thenReturn(1);
-
-        File firstCachedJarFile = FileUtil.newFile(
-                _appdir.getRoot(),
-                CacheBasedClassPathBuilder.CACHE_DIR, "fi", "first.jar");
-
-        File secondCachedJarFile = FileUtil.newFile(
-                _appdir.getRoot(),
-                CacheBasedClassPathBuilder.CACHE_DIR, "se", "second.jar");
-
-        String expectedClassPath = firstCachedJarFile.getAbsolutePath()
-                + File.pathSeparator
-                + secondCachedJarFile.getAbsolutePath();
-
-        ClassPath classPath = new CacheBasedClassPathBuilder(_application).buildClassPath();
-
-        assertEquals(expectedClassPath, classPath.asArgumentString());
-    }
-}
diff --git a/src/test/java/com/threerings/getdown/classpath/ClassPathBuilderFactoryTest.java b/src/test/java/com/threerings/getdown/classpath/ClassPathBuilderFactoryTest.java
deleted file mode 100644
index d16e5df..0000000
--- a/src/test/java/com/threerings/getdown/classpath/ClassPathBuilderFactoryTest.java
+++ /dev/null
@@ -1,37 +0,0 @@
-package com.threerings.getdown.classpath;
-
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.when;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.runners.MockitoJUnitRunner;
-
-import com.threerings.getdown.data.Application;
-
-/**
- * Checks wether the correct {@link ClassPathBuilder class path builders} are created.
- */
-@RunWith(MockitoJUnitRunner.class)
-public class ClassPathBuilderFactoryTest
-{
-    @Test
-    public void shouldCreateDefaultClassPathBuilder ()
-    {
-        when(_application.isUseCodeCache()).thenReturn(Boolean.FALSE);
-
-        assertTrue(ClassPathBuilderFactory.create(_application) instanceof DefaultClassPathBuilder);
-    }
-
-    @Test
-    public void shouldCreateCacheClassPathBuilder ()
-    {
-        when(_application.isUseCodeCache()).thenReturn(Boolean.TRUE);
-
-        assertTrue(ClassPathBuilderFactory.create(_application) instanceof CacheBasedClassPathBuilder);
-    }
-
-    @Mock
-    private Application _application;
-}
diff --git a/src/test/java/com/threerings/getdown/classpath/ClassPathBuilderTestBase.java b/src/test/java/com/threerings/getdown/classpath/ClassPathBuilderTestBase.java
deleted file mode 100644
index a6a80e2..0000000
--- a/src/test/java/com/threerings/getdown/classpath/ClassPathBuilderTestBase.java
+++ /dev/null
@@ -1,48 +0,0 @@
-package com.threerings.getdown.classpath;
-
-import static org.mockito.Mockito.when;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.Arrays;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.rules.TemporaryFolder;
-import org.mockito.Mock;
-
-import com.threerings.getdown.data.Application;
-import com.threerings.getdown.data.Resource;
-
-/**
- * Base class for {@link ClassPathBuilder} tests.
- */
-public abstract class ClassPathBuilderTestBase {
-    @Before
-    public void setupFilesAndResources () throws IOException
-    {
-        _firstJarFile = _appdir.newFile("a.jar");
-        _secondJarFile = _appdir.newFile("b.jar");
-
-        when(_firstJar.getFinalTarget()).thenReturn(_firstJarFile);
-        when(_secondJar.getFinalTarget()).thenReturn(_secondJarFile);
-        when(_application.getActiveCodeResources()).thenReturn(Arrays.asList(_firstJar, _secondJar));
-        when(_application.getAppdir()).thenReturn(_appdir.getRoot());
-    }
-
-    @Mock
-    protected Application _application;
-
-    @Mock
-    protected Resource _firstJar;
-
-    protected File _firstJarFile;
-
-    @Mock
-    protected Resource _secondJar;
-
-    protected File _secondJarFile;
-
-    @Rule
-    public TemporaryFolder _appdir = new TemporaryFolder();
-}
diff --git a/src/test/java/com/threerings/getdown/classpath/ClassPathTest.java b/src/test/java/com/threerings/getdown/classpath/ClassPathTest.java
index aaf9e9a..fb90a80 100644
--- a/src/test/java/com/threerings/getdown/classpath/ClassPathTest.java
+++ b/src/test/java/com/threerings/getdown/classpath/ClassPathTest.java
@@ -1,7 +1,5 @@
 package com.threerings.getdown.classpath;
 
-import static org.junit.Assert.assertEquals;
-
 import java.io.File;
 import java.io.IOException;
 import java.net.MalformedURLException;
@@ -9,52 +7,43 @@ import java.net.URISyntaxException;
 import java.net.URL;
 import java.util.LinkedHashSet;
 
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
+import org.junit.*;
 import org.junit.rules.TemporaryFolder;
 
+import static org.junit.Assert.assertEquals;
+
 /**
  * Tests for {@link ClassPath}.
  */
 public class ClassPathTest
 {
-    @Before
-    public void createJarsAndSetupClassPath () throws IOException
+    @Before public void createJarsAndSetupClassPath () throws IOException
     {
         _firstJar = _folder.newFile("a.jar");
         _secondJar = _folder.newFile("b.jar");
 
-        LinkedHashSet classPathEntries = new LinkedHashSet();
-
-        classPathEntries.add(new ClassPathElement(_firstJar));
-        classPathEntries.add(new ClassPathElement(_secondJar));
-
+        LinkedHashSet classPathEntries = new LinkedHashSet();
+        classPathEntries.add(_firstJar);
+        classPathEntries.add(_secondJar);
         _classPath = new ClassPath(classPathEntries);
     }
 
-    @Test
-    public void shouldCreateValidArgumentString ()
+    @Test public void shouldCreateValidArgumentString ()
     {
         assertEquals(
-                _firstJar.getAbsolutePath() + File.pathSeparator + _secondJar.getAbsolutePath(),
-                _classPath.asArgumentString());
+            _firstJar.getAbsolutePath() + File.pathSeparator + _secondJar.getAbsolutePath(),
+            _classPath.asArgumentString());
     }
 
-    @Test
-    public void shouldProvideJarUrls () throws MalformedURLException, URISyntaxException
+    @Test public void shouldProvideJarUrls () throws MalformedURLException, URISyntaxException
     {
         URL[] actualUrls = _classPath.asUrls();
-
         assertEquals(_firstJar, new File(actualUrls[0].toURI()));
         assertEquals(_secondJar, new File(actualUrls[1].toURI()));
     }
 
-    @Rule
-    public TemporaryFolder _folder = new TemporaryFolder();
-
-    private File _firstJar;
-    private File _secondJar;
+    @Rule public TemporaryFolder _folder = new TemporaryFolder();
 
+    private File _firstJar, _secondJar;
     private ClassPath _classPath;
 }
diff --git a/src/test/java/com/threerings/getdown/classpath/ClassPathsTest.java b/src/test/java/com/threerings/getdown/classpath/ClassPathsTest.java
new file mode 100644
index 0000000..956585a
--- /dev/null
+++ b/src/test/java/com/threerings/getdown/classpath/ClassPathsTest.java
@@ -0,0 +1,68 @@
+package com.threerings.getdown.classpath;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+
+import org.junit.*;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import static org.junit.Assert.assertEquals;
+
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import static org.mockito.Mockito.when;
+
+import com.threerings.getdown.data.Application;
+import com.threerings.getdown.data.Resource;
+import com.threerings.getdown.util.FileUtil;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ClassPathsTest
+{
+    @Before public void setupFilesAndResources () throws IOException
+    {
+        _firstJarFile = _appdir.newFile("a.jar");
+        _secondJarFile = _appdir.newFile("b.jar");
+
+        when(_firstJar.getFinalTarget()).thenReturn(_firstJarFile);
+        when(_secondJar.getFinalTarget()).thenReturn(_secondJarFile);
+        when(_application.getActiveCodeResources()).thenReturn(Arrays.asList(_firstJar, _secondJar));
+        when(_application.getAppdir()).thenReturn(_appdir.getRoot());
+    }
+
+    @Test public void shouldBuildDefaultClassPath () throws IOException
+    {
+        ClassPath classPath = ClassPaths.buildDefaultClassPath(_application);
+        String expectedClassPath = _firstJarFile.getAbsolutePath() + File.pathSeparator +
+            _secondJarFile.getAbsolutePath();
+        assertEquals(expectedClassPath, classPath.asArgumentString());
+    }
+
+    @Test public void shouldBuildCachedClassPath () throws IOException
+    {
+        when(_application.getDigest(_firstJar)).thenReturn("first");
+        when(_application.getDigest(_secondJar)).thenReturn("second");
+        when(_application.getCodeCacheRetentionDays()).thenReturn(1);
+
+        File firstCachedJarFile = FileUtil.newFile(
+            _appdir.getRoot(), ClassPaths.CACHE_DIR, "fi", "first.jar");
+
+        File secondCachedJarFile = FileUtil.newFile(
+            _appdir.getRoot(), ClassPaths.CACHE_DIR, "se", "second.jar");
+
+        String expectedClassPath = firstCachedJarFile.getAbsolutePath() + File.pathSeparator +
+            secondCachedJarFile.getAbsolutePath();
+
+        ClassPath classPath = ClassPaths.buildCachedClassPath(_application);
+        assertEquals(expectedClassPath, classPath.asArgumentString());
+    }
+
+    @Mock protected Application _application;
+    @Mock protected Resource _firstJar;
+    @Mock protected Resource _secondJar;
+
+    protected File _firstJarFile, _secondJarFile;
+
+    @Rule public TemporaryFolder _appdir = new TemporaryFolder();
+}
diff --git a/src/test/java/com/threerings/getdown/classpath/DefaultClassPathBuilderTest.java b/src/test/java/com/threerings/getdown/classpath/DefaultClassPathBuilderTest.java
deleted file mode 100644
index 96a088e..0000000
--- a/src/test/java/com/threerings/getdown/classpath/DefaultClassPathBuilderTest.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package com.threerings.getdown.classpath;
-
-import static org.junit.Assert.assertEquals;
-
-import java.io.File;
-import java.io.IOException;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.runners.MockitoJUnitRunner;
-
-/**
- * Tests the {@link DefaultClassPathBuilder}.
- */
-@RunWith(MockitoJUnitRunner.class)
-public class DefaultClassPathBuilderTest extends ClassPathBuilderTestBase
-{
-    @Test
-    public void shouldBuildClassPath () throws IOException
-    {
-        ClassPath classPath = new DefaultClassPathBuilder(_application).buildClassPath();
-
-        String expectedClassPath = _firstJarFile.getAbsolutePath()
-                + File.pathSeparator
-                + _secondJarFile.getAbsolutePath();
-
-        assertEquals(expectedClassPath, classPath.asArgumentString());
-    }
-
-
-}
diff --git a/src/test/java/com/threerings/getdown/classpath/cache/GarbageCollectorTest.java b/src/test/java/com/threerings/getdown/classpath/cache/GarbageCollectorTest.java
index 1a9975d..ee86685 100644
--- a/src/test/java/com/threerings/getdown/classpath/cache/GarbageCollectorTest.java
+++ b/src/test/java/com/threerings/getdown/classpath/cache/GarbageCollectorTest.java
@@ -1,90 +1,65 @@
 package com.threerings.getdown.classpath.cache;
 
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assume.assumeTrue;
-
 import java.io.File;
 import java.io.IOException;
 import java.util.concurrent.TimeUnit;
 
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
+import org.junit.*;
 import org.junit.rules.TemporaryFolder;
 
-import com.threerings.getdown.util.file.FileWalker;
+import static org.junit.Assert.*;
+import static org.junit.Assume.assumeTrue;
 
 /**
  * Validates that cache garbage is collected and deleted correctly.
  */
 public class GarbageCollectorTest
 {
-    @Before
-    public void setupFiles () throws IOException
+    @Before public void setupFiles () throws IOException
     {
         _cachedFile = _folder.newFile("abc123.jar");
         _lastAccessedFile = _folder.newFile("abc123.jar" + ResourceCache.LAST_ACCESSED_FILE_SUFFIX);
     }
 
-    @Test
-    public void shouldDeleteCacheEntryIfRetentionPeriodIsReached ()
+    @Test public void shouldDeleteCacheEntryIfRetentionPeriodIsReached ()
     {
         gcNow();
-
         assertFalse(_cachedFile.exists());
         assertFalse(_lastAccessedFile.exists());
     }
 
-    @Test
-    public void shouldDeleteCacheFolderIfFolderIsEmpty ()
+    @Test public void shouldDeleteCacheFolderIfFolderIsEmpty ()
     {
         gcNow();
-
         assertFalse(_folder.getRoot().exists());
     }
 
     private void gcNow() {
-        GarbageCollector collector = new GarbageCollector(
-                new FileWalker(_folder.getRoot()), -1, TimeUnit.MILLISECONDS);
-
-        collector.collectGarbage();
+        GarbageCollector.collect(_folder.getRoot(), -1);
     }
 
-    @Test
-    public void shouldKeepFilesInCacheIfRententionPeriodIsNotReached ()
+    @Test public void shouldKeepFilesInCacheIfRententionPeriodIsNotReached ()
     {
-        GarbageCollector collector = new GarbageCollector(
-                new FileWalker(_folder.getRoot()), 1, TimeUnit.DAYS);
-
-        collector.collectGarbage();
-
+        GarbageCollector.collect(_folder.getRoot(), TimeUnit.DAYS.toMillis(1));
         assertTrue(_cachedFile.exists());
         assertTrue(_lastAccessedFile.exists());
     }
 
-    @Test
-    public void shouldDeleteCachedFileIfLastAccessedFileIsMissing ()
+    @Test public void shouldDeleteCachedFileIfLastAccessedFileIsMissing ()
     {
         assumeTrue(_lastAccessedFile.delete());
-
         gcNow();
-
         assertFalse(_cachedFile.exists());
     }
 
-    @Test
-    public void shouldDeleteLastAccessedFileIfCachedFileIsMissing ()
+    @Test public void shouldDeleteLastAccessedFileIfCachedFileIsMissing ()
     {
         assumeTrue(_cachedFile.delete());
-
         gcNow();
-
         assertFalse(_lastAccessedFile.exists());
     }
 
-    @Rule
-    public TemporaryFolder _folder = new TemporaryFolder();
+    @Rule public TemporaryFolder _folder = new TemporaryFolder();
 
     private File _cachedFile;
     private File _lastAccessedFile;
diff --git a/src/test/java/com/threerings/getdown/classpath/cache/ResourceCacheTest.java b/src/test/java/com/threerings/getdown/classpath/cache/ResourceCacheTest.java
index dd589d8..8002f0b 100644
--- a/src/test/java/com/threerings/getdown/classpath/cache/ResourceCacheTest.java
+++ b/src/test/java/com/threerings/getdown/classpath/cache/ResourceCacheTest.java
@@ -1,30 +1,25 @@
 package com.threerings.getdown.classpath.cache;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-
 import java.io.File;
 import java.io.IOException;
 import java.util.concurrent.TimeUnit;
 
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
+import org.junit.*;
 import org.junit.rules.TemporaryFolder;
 
+import static org.junit.Assert.*;
+
 /**
  * Asserts the correct functionality of the {@link ResourceCache}.
  */
 public class ResourceCacheTest
 {
-    @Before
-    public void setupCache () throws IOException {
+    @Before public void setupCache () throws IOException {
         _fileToCache = _folder.newFile("filetocache.jar");
         _cache = new ResourceCache(_folder.newFolder(".cache"));
     }
 
-    @Test
-    public void shouldCacheFile () throws IOException
+    @Test public void shouldCacheFile () throws IOException
     {
         assertEquals("abc123.jar", cacheFile().getName());
     }
@@ -34,53 +29,36 @@ public class ResourceCacheTest
         return _cache.cacheFile(_fileToCache, "abc123");
     }
 
-    @Test
-    public void shouldTrackFileUsage () throws IOException
+    @Test public void shouldTrackFileUsage () throws IOException
     {
-        File lastAccessedFile = new File(
-                cacheFile().getParentFile(),
-                "abc123.jar" + ResourceCache.LAST_ACCESSED_FILE_SUFFIX);
-
+        String name = "abc123.jar" + ResourceCache.LAST_ACCESSED_FILE_SUFFIX;
+        File lastAccessedFile = new File(cacheFile().getParentFile(), name);
         assertTrue(lastAccessedFile.exists());
     }
 
-    @Test
-    public void shouldNotCacheTheSameFile () throws Exception
+    @Test public void shouldNotCacheTheSameFile () throws Exception
     {
         File cachedFile = cacheFile();
-
         cachedFile.setLastModified(YESTERDAY);
-
         long expectedLastModified = cachedFile.lastModified();
-
         // caching it another time
         File sameCachedFile = cacheFile();
-
         assertEquals(expectedLastModified, sameCachedFile.lastModified());
     }
 
-    @Test
-    public void shouldRememberWhenFileWasRequested () throws Exception
+    @Test public void shouldRememberWhenFileWasRequested () throws Exception
     {
         File cachedFile = cacheFile();
-
-        File lastAccessedFile = new File(
-                cachedFile.getParentFile(),
-                cachedFile.getName() + ResourceCache.LAST_ACCESSED_FILE_SUFFIX);
-
+        String name = cachedFile.getName() + ResourceCache.LAST_ACCESSED_FILE_SUFFIX;
+        File lastAccessedFile = new File(cachedFile.getParentFile(), name);
         lastAccessedFile.setLastModified(YESTERDAY);
-
         long lastAccessed = lastAccessedFile.lastModified();
-
         // caching it another time
         cacheFile();
-
-
         assertTrue(lastAccessedFile.lastModified() > lastAccessed);
     }
 
-    @Rule
-    public TemporaryFolder _folder = new TemporaryFolder();
+    @Rule public TemporaryFolder _folder = new TemporaryFolder();
 
     private File _fileToCache;
     private ResourceCache _cache;
diff --git a/src/test/java/com/threerings/getdown/util/FileUtilTest.java b/src/test/java/com/threerings/getdown/util/FileUtilTest.java
index 30214bf..b9d68cd 100644
--- a/src/test/java/com/threerings/getdown/util/FileUtilTest.java
+++ b/src/test/java/com/threerings/getdown/util/FileUtilTest.java
@@ -5,49 +5,56 @@
 
 package com.threerings.getdown.util;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
 import java.io.File;
 import java.io.IOException;
 import java.io.StringReader;
 import java.util.List;
 
-import org.junit.Rule;
-import org.junit.Test;
+import org.junit.*;
 import org.junit.rules.TemporaryFolder;
 
+import static org.junit.Assert.*;
+
 /**
  * Tests {@link FileUtil}.
  */
 public class FileUtilTest
 {
-    @Test
-    public void testReadLines () throws IOException
+    @Test public void testReadLines () throws IOException
     {
         String data = "This is a test\nof a file with\na few lines\n";
         List lines = FileUtil.readLines(new StringReader(data));
         String[] linesBySplit = data.split("\n");
         assertEquals(linesBySplit.length, lines.size());
-        for (int i = 0; i < lines.size(); i++) {
-            assertEquals(linesBySplit[i], lines.get(i));
+        for (int ii = 0; ii < lines.size(); ii++) {
+            assertEquals(linesBySplit[ii], lines.get(ii));
         }
     }
 
-    @Test
-    public void shouldCopyFile () throws IOException
+    @Test public void shouldCopyFile () throws IOException
     {
         File source = _folder.newFile("source.txt");
         File target = new File(_folder.getRoot(), "target.txt");
-
         assertFalse(target.exists());
-
         FileUtil.copy(source, target);
-
         assertTrue(target.exists());
     }
 
-    @Rule
-    public TemporaryFolder _folder = new TemporaryFolder();
+    @Test public void shouldRecursivelyWalkOverFilesAndFolders () throws IOException
+    {
+        _folder.newFile("a.txt");
+        new File(_folder.newFolder("b"), "b.txt").createNewFile();
+
+        class CountingVisitor implements FileUtil.Visitor {
+            int fileCount = 0;
+            @Override public void visit(File file) {
+                fileCount++;
+            }
+        }
+        CountingVisitor visitor = new CountingVisitor();
+        FileUtil.walkTree(_folder.getRoot(), visitor);
+        assertEquals(3, visitor.fileCount);
+    }
+
+    @Rule public TemporaryFolder _folder = new TemporaryFolder();
 }
diff --git a/src/test/java/com/threerings/getdown/util/file/FileWalkerTest.java b/src/test/java/com/threerings/getdown/util/file/FileWalkerTest.java
deleted file mode 100644
index c4787e0..0000000
--- a/src/test/java/com/threerings/getdown/util/file/FileWalkerTest.java
+++ /dev/null
@@ -1,41 +0,0 @@
-package com.threerings.getdown.util.file;
-
-import static org.junit.Assert.assertEquals;
-
-import java.io.File;
-import java.io.IOException;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-
-/**
- * Class under test: {@link FileWalker}.
- */
-public class FileWalkerTest
-{
-    @Test
-    public void shouldRecursivelyWalkOverFilesAndFolders () throws IOException
-    {
-        _folder.newFile("a.txt");
-        new File(_folder.newFolder("b"), "b.txt").createNewFile();
-
-        CountingVisitor visitor = new CountingVisitor();
-        new FileWalker(_folder.getRoot()).walkTree(visitor);
-
-        assertEquals(3, visitor.fileCount);
-    }
-
-    public static class CountingVisitor implements FileVisitor
-    {
-        int fileCount = 0;
-
-        @Override
-        public void visit(File file) {
-            fileCount++;
-        }
-    }
-
-    @Rule
-    public TemporaryFolder _folder = new TemporaryFolder();
-}