Merge branch 'feature/JAL-3187linkedFeatures' into feature/JAL-3251biotypedMappings
[jalview.git] / getdown / src / getdown / core / src / main / java / com / threerings / getdown / data / Resource.java
diff --git a/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Resource.java b/getdown/src/getdown/core/src/main/java/com/threerings/getdown/data/Resource.java
new file mode 100644 (file)
index 0000000..adc2d4f
--- /dev/null
@@ -0,0 +1,415 @@
+//
+// 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.*;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.security.MessageDigest;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+
+import org.apache.commons.compress.archivers.ArchiveInputStream;
+import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
+import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
+
+import com.threerings.getdown.util.FileUtil;
+import com.threerings.getdown.util.ProgressObserver;
+import com.threerings.getdown.util.StringUtil;
+
+import static com.threerings.getdown.Log.log;
+
+/**
+ * Models a single file resource used by an {@link Application}.
+ */
+public class Resource implements Comparable<Resource>
+{
+    /** Defines special attributes for resources. */
+    public static enum Attr {
+        /** Indicates that the resource should be unpacked. */
+        UNPACK,
+        /** If present, when unpacking a resource, any directories created by the newly
+          * unpacked resource will first be cleared of files before unpacking. */
+        CLEAN,
+        /** Indicates that the resource should be marked executable. */
+        EXEC,
+        /** Indicates that the resource should be downloaded before a UI is displayed. */
+        PRELOAD,
+        /** Indicates that the resource is a jar containing native libs. */
+        NATIVE
+    };
+
+    public static final EnumSet<Attr> NORMAL  = EnumSet.noneOf(Attr.class);
+    public static final EnumSet<Attr> UNPACK  = EnumSet.of(Attr.UNPACK);
+    public static final EnumSet<Attr> EXEC    = EnumSet.of(Attr.EXEC);
+    public static final EnumSet<Attr> PRELOAD = EnumSet.of(Attr.PRELOAD);
+    public static final EnumSet<Attr> NATIVE  = EnumSet.of(Attr.NATIVE);
+
+    /**
+     * Computes the MD5 hash of the supplied file.
+     * @param version the version of the digest protocol to use.
+     */
+    public static String computeDigest (int version, File target, MessageDigest md,
+                                        ProgressObserver obs)
+        throws IOException
+    {
+        md.reset();
+        byte[] buffer = new byte[DIGEST_BUFFER_SIZE];
+        int read;
+
+        boolean isJar = isJar(target.getPath());
+        boolean isPacked200Jar = isPacked200Jar(target.getPath());
+
+        // if this is a jar, we need to compute the digest in a "timestamp and file order" agnostic
+        // manner to properly correlate jardiff patched jars with their unpatched originals
+        if (isJar || isPacked200Jar){
+            File tmpJarFile = null;
+            JarFile jar = null;
+            try {
+                // if this is a compressed jar file, uncompress it to compute the jar file digest
+                if (isPacked200Jar){
+                    tmpJarFile = new File(target.getPath() + ".tmp");
+                    FileUtil.unpackPacked200Jar(target, tmpJarFile);
+                    jar = new JarFile(tmpJarFile);
+                } else{
+                    jar = new JarFile(target);
+                }
+
+                List<JarEntry> entries = Collections.list(jar.entries());
+                Collections.sort(entries, ENTRY_COMP);
+
+                int eidx = 0;
+                for (JarEntry entry : entries) {
+                    // old versions of the digest code skipped metadata
+                    if (version < 2) {
+                        if (entry.getName().startsWith("META-INF")) {
+                            updateProgress(obs, eidx, entries.size());
+                            continue;
+                        }
+                    }
+
+                    try (InputStream in = jar.getInputStream(entry)) {
+                        while ((read = in.read(buffer)) != -1) {
+                            md.update(buffer, 0, read);
+                        }
+                    }
+
+                    updateProgress(obs, eidx, entries.size());
+                }
+
+            } finally {
+                if (jar != null) {
+                    try {
+                        jar.close();
+                    } catch (IOException ioe) {
+                        log.warning("Error closing jar", "path", target, "jar", jar, "error", ioe);
+                    }
+                }
+                if (tmpJarFile != null) {
+                    FileUtil.deleteHarder(tmpJarFile);
+                }
+            }
+
+        } else {
+            long totalSize = target.length(), position = 0L;
+            try (FileInputStream fin = new FileInputStream(target)) {
+                while ((read = fin.read(buffer)) != -1) {
+                    md.update(buffer, 0, read);
+                    position += read;
+                    updateProgress(obs, position, totalSize);
+                }
+            }
+        }
+        return StringUtil.hexlate(md.digest());
+    }
+
+    /**
+     * Creates a resource with the supplied remote URL and local path.
+     */
+    public Resource (String path, URL remote, File local, EnumSet<Attr> attrs)
+    {
+        _path = path;
+        _remote = remote;
+        _local = local;
+        _localNew = new File(local.toString() + "_new");
+        String lpath = _local.getPath();
+        _marker = new File(lpath + "v");
+
+        _attrs = attrs;
+        _isTgz = isTgz(lpath);
+        _isJar = isJar(lpath);
+        _isPacked200Jar = isPacked200Jar(lpath);
+        boolean unpack = attrs.contains(Attr.UNPACK);
+        if (unpack && _isJar) {
+            _unpacked = _local.getParentFile();
+        } else if(unpack && _isTgz) {
+            _unpacked = _local.getParentFile();
+        } else if(unpack && _isPacked200Jar) {
+            String dotJar = ".jar", lname = _local.getName();
+            String uname = lname.substring(0, lname.lastIndexOf(dotJar) + dotJar.length());
+            _unpacked = new File(_local.getParent(), uname);
+        }
+    }
+
+    /**
+     * Returns the path associated with this resource.
+     */
+    public String getPath ()
+    {
+        return _path;
+    }
+
+    /**
+     * Returns the local location of this resource.
+     */
+    public File getLocal ()
+    {
+        return _local;
+    }
+
+    /**
+     * Returns the location of the to-be-installed new version of this resource.
+     */
+    public File getLocalNew ()
+    {
+        return _localNew;
+    }
+
+    /**
+     *  Returns the location of the unpacked resource.
+     */
+    public File getUnpacked ()
+    {
+        return _unpacked;
+    }
+
+    /**
+     *  Returns the final target of this resource, whether it has been unpacked or not.
+     */
+    public File getFinalTarget ()
+    {
+        return shouldUnpack() ? getUnpacked() : getLocal();
+    }
+
+    /**
+     * Returns the remote location of this resource.
+     */
+    public URL getRemote ()
+    {
+        return _remote;
+    }
+
+    /**
+     * Returns true if this resource should be unpacked as a part of the validation process.
+     */
+    public boolean shouldUnpack ()
+    {
+        return _attrs.contains(Attr.UNPACK) && !SysProps.noUnpack();
+    }
+
+    /**
+     * Returns true if this resource should be pre-downloaded.
+     */
+    public boolean shouldPredownload ()
+    {
+        return _attrs.contains(Attr.PRELOAD);
+    }
+
+    /**
+     * Returns true if this resource is a native lib jar.
+     */
+    public boolean isNative ()
+    {
+        return _attrs.contains(Attr.NATIVE);
+    }
+
+    /**
+     * Computes the MD5 hash of this resource's underlying file.
+     * <em>Note:</em> This is both CPU and I/O intensive.
+     * @param version the version of the digest protocol to use.
+     */
+    public String computeDigest (int version, MessageDigest md, ProgressObserver obs)
+        throws IOException
+    {
+        File file;
+        if (_local.toString().toLowerCase(Locale.ROOT).endsWith(Application.CONFIG_FILE)) {
+            file = _local;
+        } else {
+            file = _localNew.exists() ? _localNew : _local;
+        }
+        return computeDigest(version, file, md, obs);
+    }
+
+    /**
+     * Returns true if this resource has an associated "validated" marker
+     * file.
+     */
+    public boolean isMarkedValid ()
+    {
+        if (!_local.exists()) {
+            clearMarker();
+            return false;
+        }
+        return _marker.exists();
+    }
+
+    /**
+     * Creates a "validated" marker file for this resource to indicate
+     * that its MD5 hash has been computed and compared with the value in
+     * the digest file.
+     *
+     * @throws IOException if we fail to create the marker file.
+     */
+    public void markAsValid ()
+        throws IOException
+    {
+        _marker.createNewFile();
+    }
+
+    /**
+     * Removes any "validated" marker file associated with this resource.
+     */
+    public void clearMarker ()
+    {
+        if (_marker.exists() && !FileUtil.deleteHarder(_marker)) {
+            log.warning("Failed to erase marker file '" + _marker + "'.");
+        }
+    }
+
+    /**
+     * Installs the {@code getLocalNew} version of this resource to {@code getLocal}.
+     * @param validate whether or not to mark the resource as valid after installing.
+     */
+    public void install (boolean validate) throws IOException {
+        File source = getLocalNew(), dest = getLocal();
+        log.info("- " + source);
+        if (!FileUtil.renameTo(source, dest)) {
+            throw new IOException("Failed to rename " + source + " to " + dest);
+        }
+        applyAttrs();
+        if (validate) {
+            markAsValid();
+        }
+    }
+
+    /**
+     * Unpacks this resource file into the directory that contains it.
+     */
+    public void unpack () throws IOException
+    {
+        // sanity check
+        if (!_isJar && !_isPacked200Jar && !_isTgz) {
+            throw new IOException("Requested to unpack non-jar/tgz file '" + _local + "'.");
+        }
+        if (_isJar) {
+            try (JarFile jar = new JarFile(_local)) {
+                FileUtil.unpackJar(jar, _unpacked, _attrs.contains(Attr.CLEAN));
+            }
+        } else if (_isTgz) {
+            try (InputStream fi = Files.newInputStream(_local.toPath());
+                         InputStream bi = new BufferedInputStream(fi);
+                         InputStream gzi = new GzipCompressorInputStream(bi);
+                         TarArchiveInputStream tgz = new TarArchiveInputStream(gzi)) {
+                    FileUtil.unpackTgz(tgz, _unpacked, _attrs.contains(Attr.CLEAN));
+            }
+        } else {
+            FileUtil.unpackPacked200Jar(_local, _unpacked);
+        }
+    }
+
+    /**
+     * Applies this resources special attributes: unpacks this resource if needed, marks it as
+     * executable if needed.
+     */
+    public void applyAttrs () throws IOException {
+        if (shouldUnpack()) {
+            unpack();
+        }
+        if (_attrs.contains(Attr.EXEC)) {
+            FileUtil.makeExecutable(_local);
+        }
+    }
+
+    /**
+     * Wipes this resource file along with any "validated" marker file that may be associated with
+     * it.
+     */
+    public void erase ()
+    {
+        clearMarker();
+        if (_local.exists() && !FileUtil.deleteHarder(_local)) {
+            log.warning("Failed to erase resource '" + _local + "'.");
+        }
+    }
+
+    @Override public int compareTo (Resource other) {
+        return _path.compareTo(other._path);
+    }
+
+    @Override public boolean equals (Object other)
+    {
+        if (other instanceof Resource) {
+            return _path.equals(((Resource)other)._path);
+        } else {
+            return false;
+        }
+    }
+
+    @Override public int hashCode ()
+    {
+        return _path.hashCode();
+    }
+
+    @Override public String toString ()
+    {
+        return _path;
+    }
+
+    /** Helper function to simplify the process of reporting progress. */
+    protected static void updateProgress (ProgressObserver obs, long pos, long total)
+    {
+        if (obs != null) {
+            obs.progress((int)(100 * pos / total));
+        }
+    }
+
+    protected static boolean isJar (String path)
+    {
+        return path.endsWith(".jar") || path.endsWith(".jar_new");
+    }
+    
+    protected static boolean isTgz (String path)
+    {
+        return path.endsWith(".tgz") || path.endsWith(".tgz_new");
+    }
+
+    protected static boolean isPacked200Jar (String path)
+    {
+        return path.endsWith(".jar.pack") || path.endsWith(".jar.pack_new") ||
+            path.endsWith(".jar.pack.gz")|| path.endsWith(".jar.pack.gz_new");
+    }
+
+    protected String _path;
+    protected URL _remote;
+    protected File _local, _localNew, _marker, _unpacked;
+    protected EnumSet<Attr> _attrs;
+    protected boolean _isJar, _isPacked200Jar, _isTgz;
+
+    /** Used to sort the entries in a jar file. */
+    protected static final Comparator<JarEntry> ENTRY_COMP = new Comparator<JarEntry>() {
+        @Override public int compare (JarEntry e1, JarEntry e2) {
+            return e1.getName().compareTo(e2.getName());
+        }
+    };
+
+    protected static final int DIGEST_BUFFER_SIZE = 5 * 1025;
+}