Merge branch 'feature/JAL-3187linkedFeatures' into feature/JAL-3251biotypedMappings
[jalview.git] / getdown / src / getdown / core / src / main / java / com / threerings / getdown / data / PathBuilder.java
diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/PathBuilder.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/PathBuilder.java
new file mode 100644 (file)
index 0000000..b0a1dc9
--- /dev/null
@@ -0,0 +1,135 @@
+//
+// Getdown - application installer, patcher and launcher
+// Copyright (C) 2004-2018 Getdown authors
+// https://github.com/threerings/getdown/blob/master/LICENSE
+
+package com.threerings.getdown.data;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.jar.JarFile;
+
+import com.threerings.getdown.cache.GarbageCollector;
+import com.threerings.getdown.cache.ResourceCache;
+import com.threerings.getdown.util.FileUtil;
+import static com.threerings.getdown.Log.log;
+
+public class PathBuilder
+{
+    /** Name of directory to store cached code files in. */
+    public static final String CODE_CACHE_DIR = ".cache";
+
+    /** Name of directory to store cached native resources in. */
+    public static final String NATIVE_CACHE_DIR = ".ncache";
+
+    /**
+     * Builds either a default or cached classpath based on {@code app}'s configuration.
+     */
+    public static ClassPath buildClassPath (Application app) throws IOException
+    {
+        return app.useCodeCache() ? buildCachedClassPath(app) : buildDefaultClassPath(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<File> classPathEntries = new LinkedHashSet<File>();
+        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 codeCacheDir = new File(app.getAppDir(), CODE_CACHE_DIR);
+
+        // a negative value of code_cache_retention_days allows to clean up the cache forcefully
+        long retainMillis = TimeUnit.DAYS.toMillis(app.getCodeCacheRetentionDays());
+        if (retainMillis != 0L) {
+            GarbageCollector.collect(codeCacheDir, retainMillis);
+        }
+
+        ResourceCache cache = new ResourceCache(codeCacheDir);
+        LinkedHashSet<File> classPathEntries = new LinkedHashSet<>();
+        for (Resource resource: app.getActiveCodeResources()) {
+            String digest = app.getDigest(resource);
+            File entry = cache.cacheFile(resource.getFinalTarget(), digest.substring(0, 2), digest);
+            classPathEntries.add(entry);
+        }
+
+        return new ClassPath(classPathEntries);
+    }
+
+    /**
+     * Builds a {@link ClassPath} instance by first caching all native jars (indicated by
+     * nresource=[native jar]), unpacking them, and referencing the locations of each of the
+     * unpacked files. Also performs garbage collection similar to {@link #buildCachedClassPath}
+     *
+     * @param app                   used to determine native jars and related information.
+     * @param addCurrentLibraryPath if true, it adds the locations referenced by
+     *                              {@code System.getProperty("java.library.path")} as well.
+     * @return a classpath instance if at least one native resource was found and unpacked,
+     *         {@code null} if no native resources were used by the application.
+     */
+    public static ClassPath buildLibsPath (Application app,
+                                           boolean addCurrentLibraryPath) throws IOException {
+        List<Resource> resources = app.getNativeResources();
+        if (resources.isEmpty()) {
+            return null;
+        }
+
+        LinkedHashSet<File> nativedirs = new LinkedHashSet<>();
+        File nativeCacheDir = new File(app.getAppDir(), NATIVE_CACHE_DIR);
+        ResourceCache cache = new ResourceCache(nativeCacheDir);
+
+        // negative value forces total garbage collection, 0 avoids garbage collection at all
+        long retainMillis = TimeUnit.DAYS.toMillis(app.getCodeCacheRetentionDays());
+        if (retainMillis != 0L) {
+            GarbageCollector.collectNative(nativeCacheDir, retainMillis);
+        }
+
+        for (Resource resource : resources) {
+            // Use untruncated cache subdirectory names to avoid overwriting issues when unpacking,
+            // in the off chance that two native jars share a directory AND contain files with the
+            // same names
+            String digest = app.getDigest(resource);
+            File cachedFile = cache.cacheFile(resource.getFinalTarget(), digest, digest);
+            File cachedParent = cachedFile.getParentFile();
+            File unpackedIndicator = new File(cachedParent, cachedFile.getName() + ".unpacked");
+
+            if (!unpackedIndicator.exists()) {
+                try {
+                    FileUtil.unpackJar(new JarFile(cachedFile), cachedParent, false);
+                    unpackedIndicator.createNewFile();
+                } catch (IOException ioe) {
+                    log.warning("Failed to unpack native jar",
+                                "file", cachedFile.getAbsolutePath(), ioe);
+                    // Keep going and unpack the other jars...
+                }
+            }
+
+            nativedirs.add(cachedFile.getParentFile());
+        }
+
+        if (addCurrentLibraryPath) {
+            for (String path : System.getProperty("java.library.path").split(File.pathSeparator)) {
+                nativedirs.add(new File(path));
+            }
+        }
+
+        return new ClassPath(nativedirs);
+    }
+}