classPathEntries)
+ {
+ this._classPathEntries = Collections.unmodifiableSet(classPathEntries);
+ }
+
+ /**
+ * Returns the class path as an java command line argument string, e.g
+ *
+ *
+ * /path/to/a.jar:/path/to/b.jar
+ *
+ */
+ public String asArgumentString ()
+ {
+ StringBuilder builder = new StringBuilder();
+ String delimiter = "";
+
+ for (ClassPathElement entry: _classPathEntries)
+ {
+ builder
+ .append(delimiter)
+ .append(entry.getAbsolutePath());
+
+ delimiter = File.pathSeparator;
+ }
+
+ return builder.toString();
+ }
+
+ /**
+ * Returns the class path entries as an array of URLs to be used for example by an
+ * {@link URLClassLoader}.
+ */
+ public URL[] asUrls ()
+ {
+ URL[] urls = new URL[_classPathEntries.size()];
+
+ int i = 0;
+
+ for (ClassPathElement entry : _classPathEntries) {
+ urls[i++] = entry.getURL();
+ }
+
+ return urls;
+ }
+
+ public Set getClassPathEntries ()
+ {
+ return _classPathEntries;
+ }
+
+ 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
new file mode 100644
index 0000000..a220760
--- /dev/null
+++ b/src/main/java/com/threerings/getdown/classpath/ClassPathBuilder.java
@@ -0,0 +1,13 @@
+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
new file mode 100644
index 0000000..0cc5613
--- /dev/null
+++ b/src/main/java/com/threerings/getdown/classpath/ClassPathBuilderBase.java
@@ -0,0 +1,14 @@
+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
new file mode 100644
index 0000000..5f93fdb
--- /dev/null
+++ b/src/main/java/com/threerings/getdown/classpath/ClassPathBuilderFactory.java
@@ -0,0 +1,22 @@
+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
new file mode 100644
index 0000000..edf64a2
--- /dev/null
+++ b/src/main/java/com/threerings/getdown/classpath/ClassPathElement.java
@@ -0,0 +1,47 @@
+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/DefaultClassPathBuilder.java b/src/main/java/com/threerings/getdown/classpath/DefaultClassPathBuilder.java
new file mode 100644
index 0000000..b6f375b
--- /dev/null
+++ b/src/main/java/com/threerings/getdown/classpath/DefaultClassPathBuilder.java
@@ -0,0 +1,30 @@
+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
new file mode 100644
index 0000000..6a40ae1
--- /dev/null
+++ b/src/main/java/com/threerings/getdown/classpath/cache/GarbageCollector.java
@@ -0,0 +1,91 @@
+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;
+
+/**
+ * Collects elements in the {@link ResourceCache cache} which became unused and deletes them
+ * afterwards.
+ */
+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 ()
+ {
+ _fileWalker.walkTree(new FileVisitor() {
+ @Override
+ public void visit(File file)
+ {
+ collect(file);
+ }
+ });
+ }
+
+ 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)
+ {
+ if (isLastAccessedFile(file)) {
+ return file;
+ }
+
+ return new File(
+ file.getParentFile(),
+ file.getName() + ResourceCache.LAST_ACCESSED_FILE_SUFFIX);
+ }
+
+ private boolean isLastAccessedFile (File file)
+ {
+ return file.getName().endsWith(ResourceCache.LAST_ACCESSED_FILE_SUFFIX);
+ }
+
+ private File getCachedFile (File file)
+ {
+ if (!isLastAccessedFile(file)) {
+ return file;
+ }
+
+ return 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/classpath/cache/ResourceCache.java b/src/main/java/com/threerings/getdown/classpath/cache/ResourceCache.java
new file mode 100644
index 0000000..6e35b3f
--- /dev/null
+++ b/src/main/java/com/threerings/getdown/classpath/cache/ResourceCache.java
@@ -0,0 +1,73 @@
+package com.threerings.getdown.classpath.cache;
+
+import java.io.File;
+import java.io.IOException;
+
+import com.threerings.getdown.util.FileUtil;
+
+/**
+ * Maintains a cache of code resources. The cache allows multiple application instances of different
+ * versions to open at the same time.
+ */
+public class ResourceCache
+{
+ public ResourceCache (File _cacheDir) throws IOException
+ {
+ this._cacheDir = _cacheDir;
+
+ createDirectoryIfNecessary(_cacheDir);
+ }
+
+ private void createDirectoryIfNecessary (File dir) throws IOException
+ {
+ if (!dir.exists() && !dir.mkdirs()) {
+ throw new IOException("unable to create directory: " + dir.getAbsolutePath());
+ }
+ }
+
+ /**
+ * Caches the given file under it's {@code digest}.
+ *
+ * @return the cached file
+ */
+ public File cacheFile (File fileToCache, String digest) throws IOException
+ {
+ File cacheLocation = new File(_cacheDir, digest.substring(0, 2));
+ createDirectoryIfNecessary(cacheLocation);
+
+ File cachedFile = new File(cacheLocation, digest + getFileSuffix(fileToCache));
+ File lastAccessedFile = new File(
+ cacheLocation, cachedFile.getName() + LAST_ACCESSED_FILE_SUFFIX);
+
+ if (!cachedFile.exists()) {
+ createNewFile(cachedFile);
+ FileUtil.copy(fileToCache, cachedFile);
+ }
+
+ if (lastAccessedFile.exists()) {
+ lastAccessedFile.setLastModified(System.currentTimeMillis());
+ } else {
+ createNewFile(lastAccessedFile);
+ }
+
+ return cachedFile;
+ }
+
+ private void createNewFile (File fileToCreate) throws IOException
+ {
+ if (!fileToCreate.exists() && !fileToCreate.createNewFile()) {
+ throw new IOException("unable to create new file: " + fileToCreate.getAbsolutePath());
+ }
+ }
+
+ private String getFileSuffix (File fileToCache) {
+ String fileName = fileToCache.getName();
+ int index = fileName.lastIndexOf(".");
+
+ return index > -1 ? fileName.substring(index) : "";
+ }
+
+ private final File _cacheDir;
+
+ static final String LAST_ACCESSED_FILE_SUFFIX = ".lastAccessed";
+}
diff --git a/src/main/java/com/threerings/getdown/data/Application.java b/src/main/java/com/threerings/getdown/data/Application.java
index 9b6e961..4590f64 100644
--- a/src/main/java/com/threerings/getdown/data/Application.java
+++ b/src/main/java/com/threerings/getdown/data/Application.java
@@ -5,9 +5,21 @@
package com.threerings.getdown.data;
+import static com.threerings.getdown.Log.log;
+
import java.awt.Color;
import java.awt.Rectangle;
-import java.io.*;
+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.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
@@ -16,27 +28,46 @@ import java.net.URLConnection;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.OverlappingFileLockException;
-import java.security.*;
+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.cert.Certificate;
-import java.util.*;
+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.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.ClassPath;
+import com.threerings.getdown.classpath.ClassPathBuilderFactory;
import com.threerings.getdown.launcher.RotatingBackgrounds;
-import com.threerings.getdown.util.*;
-
-import static com.threerings.getdown.Log.log;
+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;
/**
* Parses and provide access to the information contained in the getdown.txt
@@ -226,10 +257,10 @@ 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;
}
/**
@@ -352,7 +383,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));
@@ -514,7 +545,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);
@@ -531,7 +562,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");
@@ -656,6 +687,11 @@ public class Application
// obtain a thread dump of the running JVM
_windebug = getLocalPath("debug.txt").exists();
+ // 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;
+
// parse and return our application config
UpdateInterface ui = new UpdateInterface();
_name = ui.name = (String)cdata.get("ui.name");
@@ -673,9 +709,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);
@@ -690,7 +726,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
@@ -802,8 +838,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) {
@@ -901,16 +937,11 @@ public class Application
boolean dashJarMode = MANIFEST_CLASS.equals(_class);
// add the -classpath arguments if we're not in -jar mode
- StringBuilder cpbuf = new StringBuilder();
- for (Resource rsrc : getActiveCodeResources()) {
- if (cpbuf.length() > 0) {
- cpbuf.append(File.pathSeparator);
- }
- cpbuf.append(rsrc.getFinalTarget().getAbsolutePath());
- }
+ ClassPath classPath = ClassPathBuilderFactory.create(this).buildClassPath();
+
if (!dashJarMode) {
args.add("-classpath");
- args.add(cpbuf.toString());
+ args.add(classPath.asArgumentString());
}
// we love our Mac users, so we do nice things to preserve our application identity
@@ -960,7 +991,7 @@ public class Application
// if we're in -jar mode add those arguments, otherwise add the app class name
if (dashJarMode) {
args.add("-jar");
- args.add(cpbuf.toString());
+ args.add(classPath.asArgumentString());
} else {
args.add(_class);
}
@@ -1006,20 +1037,17 @@ public class Application
/**
* Runs this application directly in the current VM.
+ * @throws IOException
*/
- public void invokeDirect (JApplet applet)
+ public void invokeDirect (JApplet applet) throws IOException
{
- // create a custom class loader
- ArrayList jars = new ArrayList();
- for (Resource rsrc : getActiveCodeResources()) {
- try {
- jars.add(new URL("file", "", rsrc.getFinalTarget().getAbsolutePath()));
- } catch (Exception e) {
- e.printStackTrace(System.err);
- }
- }
+ ClassPath classPath = ClassPathBuilderFactory.create(this).buildClassPath();
+
+ URL[] jarUrls = classPath.asUrls();
+
+ // create custom class loader
URLClassLoader loader = new URLClassLoader(
- jars.toArray(new URL[jars.size()]),
+ jarUrls,
ClassLoader.getSystemClassLoader()) {
@Override protected PermissionCollection getPermissions (CodeSource code) {
Permissions perms = new Permissions();
@@ -1030,7 +1058,9 @@ public class Application
Thread.currentThread().setContextClassLoader(loader);
log.info("Configured URL class loader:");
- for (URL url : jars) log.info(" " + url);
+ for (URL url : jarUrls) {
+ log.info(" " + url);
+ }
// configure any system properties that we can
for (String jvmarg : _jvmargs) {
@@ -1126,12 +1156,6 @@ public class Application
log.info("Verifying application: " + _vappbase);
log.info("Version: " + _version);
log.info("Class: " + _class);
-// log.info("Code: " +
-// StringUtil.toString(getCodeResources().iterator()));
-// log.info("Resources: " +
-// StringUtil.toString(getActiveResources().iterator()));
-// log.info("JVM Args: " + StringUtil.toString(_jvmargs.iterator()));
-// log.info("App Args: " + StringUtil.toString(_appargs.iterator()));
// this will read in the contents of the digest file and validate itself
try {
@@ -1145,7 +1169,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();
@@ -1299,7 +1323,7 @@ public class Application
failures.add(rsrc);
}
- return (failures.size() == 0) ? null : failures;
+ return failures.size() == 0 ? null : failures;
}
/**
@@ -1357,6 +1381,24 @@ 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.
*/
@@ -1602,7 +1644,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}. */
@@ -1647,7 +1689,7 @@ public class Application
{
String value = (String)cdata.get(name);
Color color = parseColor(value);
- return (color == null) ? def : color;
+ return color == null ? def : color;
}
/**
@@ -1670,7 +1712,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);
}
/**
@@ -1746,6 +1788,19 @@ 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;
@@ -1780,6 +1835,9 @@ public class Application
protected List _codes = new ArrayList();
protected List _resources = new ArrayList();
+ protected boolean _useCodeCache;
+ protected int _codeCacheRetentionDays;
+
protected Map _auxgroups = new HashMap();
protected Map _auxactive = new HashMap();
diff --git a/src/main/java/com/threerings/getdown/data/Digest.java b/src/main/java/com/threerings/getdown/data/Digest.java
index ecdde99..5cdddf2 100644
--- a/src/main/java/com/threerings/getdown/data/Digest.java
+++ b/src/main/java/com/threerings/getdown/data/Digest.java
@@ -5,27 +5,24 @@
package com.threerings.getdown.data;
+import static com.threerings.getdown.Log.log;
+
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.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.
@@ -148,6 +145,14 @@ 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 c957e59..bbfcb2a 100644
--- a/src/main/java/com/threerings/getdown/util/FileUtil.java
+++ b/src/main/java/com/threerings/getdown/util/FileUtil.java
@@ -5,7 +5,15 @@
package com.threerings.getdown.util;
-import java.io.*;
+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.util.ArrayList;
import java.util.List;
import java.util.jar.JarOutputStream;
@@ -14,8 +22,6 @@ import java.util.zip.GZIPInputStream;
import com.samskivert.io.StreamUtil;
-import static com.threerings.getdown.Log.log;
-
/**
* File related utilities.
*/
@@ -109,7 +115,6 @@ 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;
@@ -120,4 +125,22 @@ public class FileUtil extends com.samskivert.util.FileUtil
StreamUtil.close(packedJarIn);
}
}
+
+ /**
+ * Copies the given {@code source} file to the given {@code target}.
+ */
+ 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);
+ }
+ }
}
diff --git a/src/main/java/com/threerings/getdown/util/file/FileVisitor.java b/src/main/java/com/threerings/getdown/util/file/FileVisitor.java
new file mode 100644
index 0000000..29f01ed
--- /dev/null
+++ b/src/main/java/com/threerings/getdown/util/file/FileVisitor.java
@@ -0,0 +1,6 @@
+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
new file mode 100644
index 0000000..2ffc613
--- /dev/null
+++ b/src/main/java/com/threerings/getdown/util/file/FileWalker.java
@@ -0,0 +1,42 @@
+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
new file mode 100644
index 0000000..f76a317
--- /dev/null
+++ b/src/test/java/com/threerings/getdown/classpath/CacheBasedClassPathBuilderTest.java
@@ -0,0 +1,44 @@
+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
new file mode 100644
index 0000000..d16e5df
--- /dev/null
+++ b/src/test/java/com/threerings/getdown/classpath/ClassPathBuilderFactoryTest.java
@@ -0,0 +1,37 @@
+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
new file mode 100644
index 0000000..a6a80e2
--- /dev/null
+++ b/src/test/java/com/threerings/getdown/classpath/ClassPathBuilderTestBase.java
@@ -0,0 +1,48 @@
+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
new file mode 100644
index 0000000..aaf9e9a
--- /dev/null
+++ b/src/test/java/com/threerings/getdown/classpath/ClassPathTest.java
@@ -0,0 +1,60 @@
+package com.threerings.getdown.classpath;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.MalformedURLException;
+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.rules.TemporaryFolder;
+
+/**
+ * Tests for {@link ClassPath}.
+ */
+public class ClassPathTest
+{
+ @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));
+
+ _classPath = new ClassPath(classPathEntries);
+ }
+
+ @Test
+ public void shouldCreateValidArgumentString ()
+ {
+ assertEquals(
+ _firstJar.getAbsolutePath() + File.pathSeparator + _secondJar.getAbsolutePath(),
+ _classPath.asArgumentString());
+ }
+
+ @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;
+
+ private ClassPath _classPath;
+}
diff --git a/src/test/java/com/threerings/getdown/classpath/DefaultClassPathBuilderTest.java b/src/test/java/com/threerings/getdown/classpath/DefaultClassPathBuilderTest.java
new file mode 100644
index 0000000..96a088e
--- /dev/null
+++ b/src/test/java/com/threerings/getdown/classpath/DefaultClassPathBuilderTest.java
@@ -0,0 +1,31 @@
+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
new file mode 100644
index 0000000..1a9975d
--- /dev/null
+++ b/src/test/java/com/threerings/getdown/classpath/cache/GarbageCollectorTest.java
@@ -0,0 +1,91 @@
+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.rules.TemporaryFolder;
+
+import com.threerings.getdown.util.file.FileWalker;
+
+/**
+ * Validates that cache garbage is collected and deleted correctly.
+ */
+public class GarbageCollectorTest
+{
+ @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 ()
+ {
+ gcNow();
+
+ assertFalse(_cachedFile.exists());
+ assertFalse(_lastAccessedFile.exists());
+ }
+
+ @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();
+ }
+
+ @Test
+ public void shouldKeepFilesInCacheIfRententionPeriodIsNotReached ()
+ {
+ GarbageCollector collector = new GarbageCollector(
+ new FileWalker(_folder.getRoot()), 1, TimeUnit.DAYS);
+
+ collector.collectGarbage();
+
+ assertTrue(_cachedFile.exists());
+ assertTrue(_lastAccessedFile.exists());
+ }
+
+ @Test
+ public void shouldDeleteCachedFileIfLastAccessedFileIsMissing ()
+ {
+ assumeTrue(_lastAccessedFile.delete());
+
+ gcNow();
+
+ assertFalse(_cachedFile.exists());
+ }
+
+ @Test
+ public void shouldDeleteLastAccessedFileIfCachedFileIsMissing ()
+ {
+ assumeTrue(_cachedFile.delete());
+
+ gcNow();
+
+ assertFalse(_lastAccessedFile.exists());
+ }
+
+ @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
new file mode 100644
index 0000000..5c87e2c
--- /dev/null
+++ b/src/test/java/com/threerings/getdown/classpath/cache/ResourceCacheTest.java
@@ -0,0 +1,81 @@
+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.rules.TemporaryFolder;
+
+/**
+ * Asserts the correct functionality of the {@link ResourceCache}.
+ */
+public class ResourceCacheTest
+{
+ @Before
+ public void setupCache () throws IOException {
+ _fileToCache = _folder.newFile("filetocache.jar");
+ _cache = new ResourceCache(_folder.newFolder(".cache"));
+ }
+
+ @Test
+ public void shouldCacheFile () throws IOException
+ {
+ assertEquals("abc123.jar", cacheFile().getName());
+ }
+
+ private File cacheFile() throws IOException
+ {
+ return _cache.cacheFile(_fileToCache, "abc123");
+ }
+
+ @Test
+ public void shouldTrackFileUsage () throws IOException
+ {
+ File lastAccessedFile = new File(
+ cacheFile().getParentFile(),
+ "abc123.jar" + ResourceCache.LAST_ACCESSED_FILE_SUFFIX);
+
+ assertTrue(lastAccessedFile.exists());
+ }
+
+ @Test
+ public void shouldNotCacheTheSameFile () throws Exception
+ {
+ File cachedFile = cacheFile();
+
+ TimeUnit.MILLISECONDS.sleep(5);
+
+ // caching it another time
+ File sameCachedFile = cacheFile();
+
+ assertEquals(cachedFile.lastModified(), sameCachedFile.lastModified());
+ }
+
+ @Test
+ public void shouldRememberWhenFileWasRequested () throws Exception
+ {
+ cacheFile();
+
+ TimeUnit.MILLISECONDS.sleep(5);
+
+ // caching it another time
+ File sameCachedFile = cacheFile();
+ File lastAccessed = new File(
+ sameCachedFile.getParentFile(),
+ sameCachedFile.getName() + ResourceCache.LAST_ACCESSED_FILE_SUFFIX);
+
+ assertTrue(lastAccessed.lastModified() > sameCachedFile.lastModified());
+ }
+
+ @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 c5c64f4..30214bf 100644
--- a/src/test/java/com/threerings/getdown/util/FileUtilTest.java
+++ b/src/test/java/com/threerings/getdown/util/FileUtilTest.java
@@ -5,26 +5,49 @@
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.*;
-import static org.junit.Assert.*;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
/**
* 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 ii = 0; ii < lines.size(); ii++) {
- assertEquals(linesBySplit[ii], lines.get(ii));
+ for (int i = 0; i < lines.size(); i++) {
+ assertEquals(linesBySplit[i], lines.get(i));
}
}
+
+ @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();
}
diff --git a/src/test/java/com/threerings/getdown/util/file/FileWalkerTest.java b/src/test/java/com/threerings/getdown/util/file/FileWalkerTest.java
new file mode 100644
index 0000000..c4787e0
--- /dev/null
+++ b/src/test/java/com/threerings/getdown/util/file/FileWalkerTest.java
@@ -0,0 +1,41 @@
+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();
+}