changeset 217:211a5e73d119

Changed cache to prevent jar overwriting when update happens.
author Andrew Su <asu@redhat.com>
date Mon, 18 Apr 2011 17:38:31 -0400
parents 37f99d9fbdeb
children bc48727cfc1a
files ChangeLog netx/net/sourceforge/jnlp/cache/CacheEntry.java netx/net/sourceforge/jnlp/cache/CacheLRUWrapper.java netx/net/sourceforge/jnlp/cache/CacheUtil.java netx/net/sourceforge/jnlp/cache/ResourceTracker.java netx/net/sourceforge/jnlp/runtime/JNLPClassLoader.java netx/net/sourceforge/jnlp/runtime/JNLPRuntime.java netx/net/sourceforge/jnlp/util/FileUtils.java netx/net/sourceforge/jnlp/util/XDesktopEntry.java
diffstat 9 files changed, 624 insertions(+), 65 deletions(-) [+]
line wrap: on
line diff
--- a/ChangeLog	Mon Apr 18 15:13:23 2011 -0400
+++ b/ChangeLog	Mon Apr 18 17:38:31 2011 -0400
@@ -1,3 +1,45 @@
+2011-04-18  Andrew Su  <asu@redhat.com>
+
+	* netx/net/sourceforge/jnlp/cache/CacheEntry.java:
+	(markForDelete): New method. Adds an entry to info file specifying
+	that this file should be delete.
+	(lock): New method. Locks the info file.
+	(unlock): New method. Unlocks the info file.
+	* netx/net/sourceforge/jnlp/cache/CacheUtil.java:
+	(cacheDir, lruHandler, propertiesLockPool): New private static fields.
+	(clearCache): Changed to use static field.
+	(getCacheFile): Changed to call getCacheFileIfExist and
+	makeNewCacheFile where appropriate.
+	(getCacheFileIfExist): New method. Get the file of requested item.
+	(makeNewCacheFile): New method. Create a new location to store cache
+	file.
+	(pathToURLPath): New method. Convert the file path to the url path.
+	(cleanCache): New method. Search for redundant entries and remove
+	them.
+	(removeUntrackedDirectories): New method. Remove all untracked
+	directories.
+	(lockFile): New method. Locks the given property file.
+	(unlockFile): New method. Unlocks the property file if we locked
+	before.
+	* netx/net/sourceforge/jnlp/cache/CacheLRUWrapper.java: New class.
+	Provides wrappers for handling cache's LRU.
+	* netx/net/sourceforge/jnlp/cache/ResourceTracker.java:
+	(downloadResource): Ensure that we only allow downloading the
+	specified file once.
+	(initializeResource): Added creation of new location to store an
+	updated or new file.
+	* netx/net/sourceforge/jnlp/runtime/JNLPClassLoader.java:
+	(JNLPClassLoader): Reordered the calls since we should check
+	permission after we have the files ready.
+	* netx/net/sourceforge/jnlp/runtime/JNLPRuntime.java:
+	(markNetxRunning): Added call to CacheUtil.cleanCache() when adding
+	shutdown hooks.
+	* netx/net/sourceforge/jnlp/util/FileUtils.java:
+	(getFileLock): New method.
+	* netx/net/sourceforge/jnlp/util/XDesktopEntry.java:
+	(getContentsAsReader): Changed call from using urlToPath to
+	getCacheFile, since the directories are no longer in that location.
+
 2011-04-18  Denis Lila  <dlila@redhat.com>
 
 	* netx/net/sourceforge/jnlp/Launcher.java:
--- a/netx/net/sourceforge/jnlp/cache/CacheEntry.java	Mon Apr 18 15:13:23 2011 -0400
+++ b/netx/net/sourceforge/jnlp/cache/CacheEntry.java	Mon Apr 18 17:38:31 2011 -0400
@@ -162,4 +162,24 @@
         properties.store();
     }
 
+    /**
+     * Mark this entry for deletion at shutdown.
+     */
+    public void markForDelete() { // once marked it should not be unmarked.
+        properties.setProperty("delete", Boolean.toString(true));
+    }
+
+    /**
+     * Lock cache item.
+     */
+    protected void lock() {
+        CacheUtil.lockFile(properties);
+    }
+
+    /**
+     * Unlock cache item.
+     */
+    protected void unlock() {
+        CacheUtil.unlockFile(properties);
+    }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/netx/net/sourceforge/jnlp/cache/CacheLRUWrapper.java	Mon Apr 18 17:38:31 2011 -0400
@@ -0,0 +1,245 @@
+/* CacheLRUWrapper -- Handle LRU for cache files.
+   Copyright (C) 2011 Red Hat, Inc.
+
+This file is part of IcedTea.
+
+IcedTea is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License as published by
+the Free Software Foundation, version 2.
+
+IcedTea is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with IcedTea; see the file COPYING.  If not, write to
+the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+02110-1301 USA.
+
+Linking this library statically or dynamically with other modules is
+making a combined work based on this library.  Thus, the terms and
+conditions of the GNU General Public License cover the whole
+combination.
+
+As a special exception, the copyright holders of this library give you
+permission to link this library with independent modules to produce an
+executable, regardless of the license terms of these independent
+modules, and to copy and distribute the resulting executable under
+terms of your choice, provided that you also meet, for each linked
+independent module, the terms and conditions of the license of that
+module.  An independent module is a module which is not derived from
+or based on this library.  If you modify this library, you may extend
+this exception to your version of the library, but you are not
+obligated to do so.  If you do not wish to do so, delete this
+exception statement from your version.
+ */
+package net.sourceforge.jnlp.cache;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.channels.FileLock;
+import java.nio.channels.OverlappingFileLockException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map.Entry;
+
+import net.sourceforge.jnlp.config.DeploymentConfiguration;
+import net.sourceforge.jnlp.runtime.JNLPRuntime;
+import net.sourceforge.jnlp.util.FileUtils;
+import net.sourceforge.jnlp.util.PropertiesFile;
+
+/**
+ * This class helps maintain the ordering of most recently use items across
+ * multiple jvm instances.
+ * 
+ * @author Andrew Su (asu@redhat.com, andrew.su@utoronto.ca)
+ * 
+ */
+enum CacheLRUWrapper {
+    INSTANCE;
+
+    private int lockCount = 0;
+
+    /* lock for the file RecentlyUsed */
+    private FileLock fl = null;
+
+    /* location of cache directory */
+    private final String cacheDir = new File(JNLPRuntime.getConfiguration()
+            .getProperty(DeploymentConfiguration.KEY_USER_CACHE_DIR)).getPath();
+
+    /*
+     * back-end of how LRU is implemented This file is to keep track of the most
+     * recently used items. The items are to be kept with key = (current time
+     * accessed) followed by folder of item. value = path to file.
+     */
+    private PropertiesFile cacheOrder = new PropertiesFile(
+            new File(cacheDir + File.separator + "recently_used"));
+
+    /**
+     * Returns an instance of the policy.
+     * 
+     * @param propertiesFile
+     * @return
+     */
+    public static CacheLRUWrapper getInstance() {
+        return INSTANCE;
+    }
+
+    /**
+     * Update map for keeping track of recently used items.
+     */
+    public synchronized void load() {
+        cacheOrder.load();
+    }
+
+    /**
+     * Write file to disk.
+     */
+    public synchronized void store() {
+        cacheOrder.store();
+    }
+
+    /**
+     * This adds a new entry to file.
+     * 
+     * @param key key we want path to be associated with.
+     * @param path path to cache item.
+     * @return true if we successfully added to map, false otherwise.
+     */
+    public synchronized boolean addEntry(String key, String path) {
+        if (cacheOrder.containsKey(key)) return false;
+        cacheOrder.setProperty(key, path);
+        return true;
+    }
+
+    /**
+     * This removed an entry from our map.
+     * 
+     * @param key key we want to remove.
+     * @return true if we successfully removed key from map, false otherwise.
+     */
+    public synchronized boolean removeEntry(String key) {
+        if (!cacheOrder.containsKey(key)) return false;
+        cacheOrder.remove(key);
+        return true;
+    }
+
+    private String getIdForCacheFolder(String folder) {
+        int len = cacheDir.length();
+        int index = folder.indexOf(File.separatorChar, len + 1);
+        return folder.substring(len + 1, index);
+    }
+
+    /**
+     * This updates the given key to reflect it was recently accessed.
+     * 
+     * @param oldKey Key we wish to update.
+     * @return true if we successfully updated value, false otherwise.
+     */
+    public synchronized boolean updateEntry(String oldKey) {
+        if (!cacheOrder.containsKey(oldKey)) return false;
+        String value = cacheOrder.getProperty(oldKey);
+        String folder = getIdForCacheFolder(value);
+
+        cacheOrder.remove(oldKey);
+        cacheOrder.setProperty(Long.toString(System.currentTimeMillis()) + "," + folder, value);
+        return true;
+    }
+
+    /**
+     * Return a copy of the keys available.
+     * 
+     * @return List of Strings sorted by ascending order.
+     */
+    public synchronized List<Entry<String, String>> getLRUSortedEntries() {
+        ArrayList<Entry<String, String>> entries = new ArrayList(cacheOrder.entrySet());
+        // sort by keys in descending order.
+        Collections.sort(entries, new Comparator<Entry<String, String>>() {
+            @Override
+            public int compare(Entry<String, String> e1, Entry<String, String> e2) {
+                try {
+                    Long t1 = Long.parseLong(e1.getKey().split(",")[0]);
+                    Long t2 = Long.parseLong(e2.getKey().split(",")[0]);
+
+                    int c = t1.compareTo(t2);
+                    return c < 0 ? 1 : (c > 0 ? -1 : 0);
+                } catch (NumberFormatException e) {
+                    // Perhaps an error is too harsh. Maybe just somehow turn
+                    // caching off if this is the case.
+                    throw new InternalError("Corrupt LRU file entries");
+                }
+            }
+        });
+        return entries;
+    }
+
+    /**
+     * Lock the file to have exclusive access.
+     */
+    public synchronized void lock() {
+        try {
+            File f = cacheOrder.getStoreFile();
+            if (!f.exists()) {
+                FileUtils.createParentDir(f);
+                FileUtils.createRestrictedFile(f, true);
+            }
+            fl = FileUtils.getFileLock(f.getPath(), false, true);
+        } catch (OverlappingFileLockException e) { // if overlap we just increase the count.
+        } catch (Exception e) { // We didn't get a lock..
+            e.printStackTrace();
+        }
+        if (fl != null) lockCount++;
+    }
+
+    /**
+     * Unlock the file.
+     */
+    public synchronized void unlock() {
+        if (fl != null) {
+            lockCount--;
+            try {
+                if (lockCount == 0) {
+                    fl.release();
+                    fl.channel().close();
+                    fl = null;
+                }
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    /**
+     * Return the value of given key.
+     * 
+     * @param key
+     * @return value of given key, null otherwise.
+     */
+    public synchronized String getValue(String key) {
+        return cacheOrder.getProperty(key);
+    }
+
+    /**
+     * Test if we the key provided is in use.
+     * 
+     * @param key key to be tested.
+     * @return true if the key is in use.
+     */
+    public synchronized boolean contains(String key) {
+        return cacheOrder.contains(key);
+    }
+
+    /**
+     * Generate a key given the path to file. May or may not generate the same
+     * key given same path.
+     * 
+     * @param path Path to generate a key with.
+     * @return String representing the a key.
+     */
+    public String generateKey(String path) {
+        return System.currentTimeMillis() + "," + getIdForCacheFolder(path);
+    }
+}
--- a/netx/net/sourceforge/jnlp/cache/CacheUtil.java	Mon Apr 18 15:13:23 2011 -0400
+++ b/netx/net/sourceforge/jnlp/cache/CacheUtil.java	Mon Apr 18 17:38:31 2011 -0400
@@ -21,7 +21,14 @@
 import java.io.*;
 import java.net.*;
 import java.nio.channels.FileChannel;
-import java.util.*;
+import java.nio.channels.FileLock;
+import java.nio.channels.OverlappingFileLockException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.Set;
 import java.security.*;
 import javax.jnlp.*;
 
@@ -29,6 +36,7 @@
 import net.sourceforge.jnlp.config.DeploymentConfiguration;
 import net.sourceforge.jnlp.runtime.*;
 import net.sourceforge.jnlp.util.FileUtils;
+import net.sourceforge.jnlp.util.PropertiesFile;
 
 /**
  * Provides static methods to interact with the cache, download
@@ -39,6 +47,11 @@
  */
 public class CacheUtil {
 
+    private static final String cacheDir = new File(JNLPRuntime.getConfiguration()
+            .getProperty(DeploymentConfiguration.KEY_USER_CACHE_DIR)).getPath(); // Do this with file to standardize it.
+    private static final CacheLRUWrapper lruHandler = CacheLRUWrapper.getInstance();
+    private static final HashMap<String, FileLock> propertiesLockPool = new HashMap<String, FileLock>();
+
     /**
      * Compares a URL using string compare of its protocol, host,
      * port, path, query, and anchor.  This method avoids the host
@@ -138,8 +151,7 @@
             return;
         }
 
-        File cacheDir = new File(JNLPRuntime.getConfiguration()
-                .getProperty(DeploymentConfiguration.KEY_USER_CACHE_DIR));
+        File cacheDir = new File(CacheUtil.cacheDir);
         if (!(cacheDir.isDirectory())) {
             return;
         }
@@ -284,18 +296,95 @@
         if (!isCacheable(source, version))
             throw new IllegalArgumentException(R("CNotCacheable", source));
 
-        try {
-            String cacheDir = JNLPRuntime.getConfiguration()
-                    .getProperty(DeploymentConfiguration.KEY_USER_CACHE_DIR);
-            File localFile = urlToPath(source, cacheDir);
-            FileUtils.createParentDir(localFile);
+        File cacheFile = null;
+        synchronized (lruHandler) {
+            lruHandler.lock();
+
+            // We need to reload the cacheOrder file each time
+            // since another plugin/javaws instance may have updated it.
+            lruHandler.load();
+            cacheFile = getCacheFileIfExist(urlToPath(source, ""));
+            if (cacheFile == null) { // We did not find a copy of it.
+                cacheFile = makeNewCacheFile(source, version);
+            }
+            lruHandler.unlock();
+        }
+        return cacheFile;
+    }
+
+    /**
+     * This will return a File pointing to the location of cache item.
+     * 
+     * @param urlPath Path of cache item within cache directory.
+     * @return File if we have searched before, null otherwise.
+     */
+    private static File getCacheFileIfExist(File urlPath) {
+        synchronized (lruHandler) {
+            File cacheFile = null;
+            List<Entry<String, String>> entries = lruHandler.getLRUSortedEntries();
+            // Start searching from the most recent to least recent.
+            for (Entry<String, String> e : entries) {
+                final String key = e.getKey();
+                final String path = e.getValue();
+
+                if (path != null) {
+                    if (pathToURLPath(path).equals(urlPath.getPath())) { // Match found.
+                        cacheFile = new File(path);
+                        lruHandler.updateEntry(key);
+                        break; // Stop searching since we got newest one already.
+                    }
+                }
+            }
+            return cacheFile;
+        }
+    }
 
-            return localFile;
-        } catch (Exception ex) {
-            if (JNLPRuntime.isDebug())
-                ex.printStackTrace();
+    /**
+     * Get the path to file minus the cache directory and indexed folder.
+     */
+    private static String pathToURLPath(String path) {
+        int len = cacheDir.length();
+        int index = path.indexOf(File.separatorChar, len + 1);
+        return path.substring(index);
+    }
+
+    /**
+     * This will create a new entry for the cache item. It is however not
+     * initialized but any future calls to getCacheFile with the source and
+     * version given to here, will cause it to return this item.
+     * 
+     * @param source the source URL
+     * @param version the version id of the local file
+     * @return the file location in the cache.
+     */
+    public static File makeNewCacheFile(URL source, Version version) {
+        synchronized (lruHandler) {
+            lruHandler.lock();
+            lruHandler.load();
 
-            return null;
+            File cacheFile = null;
+            for (long i = 0; i < Long.MAX_VALUE; i++) {
+                String path = cacheDir + File.separator + i;
+                File cDir = new File(path);
+                if (!cDir.exists()) {
+                    // We can use this directory.
+                    try {
+                        cacheFile = urlToPath(source, path);
+                        FileUtils.createParentDir(cacheFile);
+                        File pf = new File(cacheFile.getPath() + ".info");
+                        FileUtils.createRestrictedFile(pf, true); // Create the info file for marking later.
+                        lruHandler.addEntry(lruHandler.generateKey(cacheFile.getPath()), cacheFile.getPath());
+                    } catch (IOException ioe) {
+                        ioe.printStackTrace();
+                    }
+
+                    break;
+                }
+            }
+
+            lruHandler.store();
+            lruHandler.unlock();
+            return cacheFile;
         }
     }
 
@@ -436,4 +525,89 @@
         }
     }
 
+    /**
+     * This will remove all old cache items.
+     */
+    public static void cleanCache() {
+        if (okToClearCache()) {
+            // First we want to figure out which stuff we need to delete.
+            HashSet<String> keep = new HashSet<String>();
+            lruHandler.load();
+
+            for (Entry<String, String> e : lruHandler.getLRUSortedEntries()) {
+                // Check if the item is contained in cacheOrder.
+                final String key = e.getKey();
+                final String value = e.getValue();
+
+                if (value != null) {
+                    PropertiesFile pf = new PropertiesFile(new File(value + ".info"));
+                    boolean delete = Boolean.parseBoolean(pf.getProperty("delete"));
+
+                    // This will get me the root directory specific to this cache item.
+                    String rStr = value.substring(cacheDir.length());
+                    rStr = cacheDir + rStr.substring(0, rStr.indexOf(File.separatorChar, 1));
+
+                    if (delete || keep.contains(rStr)) {
+                        lruHandler.removeEntry(key);
+                    } else {
+                        keep.add(value.substring(rStr.length()));
+                        keep.add(rStr); // We can just use the same map, since these two things are disjoint with each other.
+                    }
+                } else {
+                    lruHandler.removeEntry(key);
+                }
+            }
+            lruHandler.store();
+
+            removeUntrackedDirectories(keep);
+        }
+    }
+
+    private static void removeUntrackedDirectories(Set<String> keep) {
+        File temp = new File(cacheDir);
+        // Remove all folder not listed in keep.
+        for (File f : temp.listFiles()) {
+            if (f.isDirectory() && !keep.contains(f.getPath())) {
+                try {
+                    FileUtils.recursiveDelete(f, f);
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+    }
+
+    /**
+     * Lock the property file and add it to our pool of locks.
+     * 
+     * @param properties Property file to lock.
+     */
+    public static void lockFile(PropertiesFile properties) {
+        String storeFilePath = properties.getStoreFile().getPath();
+        try {
+            propertiesLockPool.put(storeFilePath, FileUtils.getFileLock(storeFilePath, false, true));
+        } catch (OverlappingFileLockException e) {
+        } catch (FileNotFoundException e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * Unlock the property file and remove it from our pool. Nothing happens if
+     * it wasn't locked.
+     * 
+     * @param properties Property file to unlock.
+     */
+    public static void unlockFile(PropertiesFile properties) {
+        File storeFile = properties.getStoreFile();
+        FileLock fl = propertiesLockPool.get(storeFile.getPath());
+        try {
+            if (fl == null) return;
+            fl.release();
+            fl.channel().close();
+            propertiesLockPool.remove(storeFile.getPath());
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
 }
--- a/netx/net/sourceforge/jnlp/cache/ResourceTracker.java	Mon Apr 18 15:13:23 2011 -0400
+++ b/netx/net/sourceforge/jnlp/cache/ResourceTracker.java	Mon Apr 18 17:38:31 2011 -0400
@@ -629,6 +629,9 @@
     private void downloadResource(Resource resource) {
         resource.fireDownloadEvent(); // fire DOWNLOADING
 
+        CacheEntry origEntry = new CacheEntry(resource.location, resource.downloadVersion); // This is where the jar file will be.
+        origEntry.lock();
+
         try {
             // create out second in case in does not exist
             URL realLocation = resource.getDownloadLocation();
@@ -666,57 +669,71 @@
                 downloadLocation = new URL(downloadLocation.toString() + ".gz");
             }
 
-            InputStream in = new BufferedInputStream(con.getInputStream());
-            OutputStream out = CacheUtil.getOutputStream(downloadLocation, resource.downloadVersion);
-            byte buf[] = new byte[1024];
-            int rlen;
+            File downloadLocationFile = CacheUtil.getCacheFile(downloadLocation, resource.downloadVersion);
+            CacheEntry downloadEntry = new CacheEntry(downloadLocation, resource.downloadVersion);
+            File finalFile = CacheUtil.getCacheFile(resource.location, resource.downloadVersion); // This is where extracted version will be, or downloaded file if not compressed.
+
+            if (!finalFile.exists()) {
+                // Make sure we don't re-download the file. however it will wait as if it was downloading.
+                // (This is fine because file is not ready yet anyways)
+                byte buf[] = new byte[1024];
+                int rlen;
+
+                InputStream in = new BufferedInputStream(con.getInputStream());
+                OutputStream out = CacheUtil.getOutputStream(downloadLocation, resource.downloadVersion);
+
+                while (-1 != (rlen = in.read(buf))) {
+                    resource.transferred += rlen;
+                    out.write(buf, 0, rlen);
+                }
+
+                in.close();
+                out.close();
+
+                // explicitly close the URLConnection.
+                if (con instanceof HttpURLConnection)
+                    ((HttpURLConnection) con).disconnect();
 
-            while (-1 != (rlen = in.read(buf))) {
-                resource.transferred += rlen;
-                out.write(buf, 0, rlen);
+                /*
+                 * If the file was compressed, uncompress it.
+                 */
+                if (packgz) {
+                    GZIPInputStream gzInputStream = new GZIPInputStream(new FileInputStream(CacheUtil
+                            .getCacheFile(downloadLocation, resource.downloadVersion)));
+                    InputStream inputStream = new BufferedInputStream(gzInputStream);
+
+                    JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(CacheUtil
+                            .getCacheFile(resource.location, resource.downloadVersion)));
+
+                    Unpacker unpacker = Pack200.newUnpacker();
+                    unpacker.unpack(inputStream, outputStream);
+
+                    outputStream.close();
+                    inputStream.close();
+                    gzInputStream.close();
+                } else if (gzip) {
+                    GZIPInputStream gzInputStream = new GZIPInputStream(new FileInputStream(CacheUtil
+                            .getCacheFile(downloadLocation, resource.downloadVersion)));
+                    InputStream inputStream = new BufferedInputStream(gzInputStream);
+
+                    BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream(CacheUtil
+                            .getCacheFile(resource.location, resource.downloadVersion)));
+
+                    while (-1 != (rlen = inputStream.read(buf))) {
+                        outputStream.write(buf, 0, rlen);
+                    }
+
+                    outputStream.close();
+                    inputStream.close();
+                    gzInputStream.close();
+                }
+            } else {
+                resource.transferred = downloadLocationFile.length();
             }
 
-            in.close();
-            out.close();
-
-            // explicitly close the URLConnection.
-            if (con instanceof HttpURLConnection)
-                ((HttpURLConnection) con).disconnect();
-
-            /*
-             * If the file was compressed, uncompress it.
-             */
-
-            if (packgz) {
-                GZIPInputStream gzInputStream = new GZIPInputStream(new FileInputStream(
-                        CacheUtil.getCacheFile(downloadLocation, resource.downloadVersion)));
-                InputStream inputStream = new BufferedInputStream(gzInputStream);
-
-                JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(
-                        CacheUtil.getCacheFile(resource.location, resource.downloadVersion)));
-
-                Unpacker unpacker = Pack200.newUnpacker();
-                unpacker.unpack(inputStream, outputStream);
-
-                outputStream.close();
-                inputStream.close();
-                gzInputStream.close();
-            } else if (gzip) {
-                GZIPInputStream gzInputStream = new GZIPInputStream(new FileInputStream(CacheUtil
-                        .getCacheFile(downloadLocation, resource.downloadVersion)));
-                InputStream inputStream = new BufferedInputStream(gzInputStream);
-
-                BufferedOutputStream outputStream = new BufferedOutputStream(
-                        new FileOutputStream(CacheUtil.getCacheFile(resource.location,
-                                resource.downloadVersion)));
-
-                while (-1 != (rlen = inputStream.read(buf))) {
-                    outputStream.write(buf, 0, rlen);
-                }
-
-                outputStream.close();
-                inputStream.close();
-                gzInputStream.close();
+            if (!downloadLocationFile.getPath().equals(finalFile.getPath())) {
+                downloadEntry.markForDelete();
+                downloadEntry.store();
             }
 
             resource.changeStatus(DOWNLOADING, DOWNLOADED);
@@ -733,6 +750,8 @@
                 lock.notifyAll(); // wake up wait's to check for completion
             }
             resource.fireDownloadEvent(); // fire ERROR
+        } finally {
+            origEntry.unlock();
         }
     }
 
@@ -743,6 +762,9 @@
     private void initializeResource(Resource resource) {
         resource.fireDownloadEvent(); // fire CONNECTING
 
+        CacheEntry entry = new CacheEntry(resource.location, resource.requestVersion);
+        entry.lock();
+
         try {
             File localFile = CacheUtil.getCacheFile(resource.location, resource.downloadVersion);
 
@@ -754,6 +776,18 @@
 
             int size = connection.getContentLength();
             boolean current = CacheUtil.isCurrent(resource.location, resource.requestVersion, connection) && resource.getUpdatePolicy() != UpdatePolicy.FORCE;
+            if (!current) {
+                if (entry.isCached()) {
+                    entry.markForDelete();
+                    entry.store();
+                    // Old entry will still exist. (but removed at cleanup)
+                    localFile = CacheUtil.makeNewCacheFile(resource.location, resource.downloadVersion);
+                    CacheEntry newEntry = new CacheEntry(resource.location, resource.requestVersion);
+                    newEntry.lock();
+                    entry.unlock();
+                    entry = newEntry;
+                }
+            }
 
             synchronized (resource) {
                 resource.localFile = localFile;
@@ -767,7 +801,6 @@
             }
 
             // update cache entry
-            CacheEntry entry = new CacheEntry(resource.location, resource.requestVersion);
             if (!current)
                 entry.initialize(connection);
 
@@ -791,6 +824,8 @@
                 lock.notifyAll(); // wake up wait's to check for completion
             }
             resource.fireDownloadEvent(); // fire ERROR
+        } finally {
+            entry.unlock();
         }
     }
 
--- a/netx/net/sourceforge/jnlp/runtime/JNLPClassLoader.java	Mon Apr 18 15:13:23 2011 -0400
+++ b/netx/net/sourceforge/jnlp/runtime/JNLPClassLoader.java	Mon Apr 18 17:38:31 2011 -0400
@@ -166,11 +166,11 @@
         // initialize extensions
         initializeExtensions();
 
+        initializeResources();
+
         // initialize permissions
         initializePermissions();
 
-        initializeResources();
-
         setSecurity();
 
         installShutdownHooks();
--- a/netx/net/sourceforge/jnlp/runtime/JNLPRuntime.java	Mon Apr 18 15:13:23 2011 -0400
+++ b/netx/net/sourceforge/jnlp/runtime/JNLPRuntime.java	Mon Apr 18 17:38:31 2011 -0400
@@ -701,6 +701,7 @@
         Runtime.getRuntime().addShutdownHook(new Thread() {
             public void run() {
                 markNetxStopped();
+                CacheUtil.cleanCache();
             }
         });
     }
--- a/netx/net/sourceforge/jnlp/util/FileUtils.java	Mon Apr 18 15:13:23 2011 -0400
+++ b/netx/net/sourceforge/jnlp/util/FileUtils.java	Mon Apr 18 17:38:31 2011 -0400
@@ -19,7 +19,11 @@
 import static net.sourceforge.jnlp.runtime.Translator.R;
 
 import java.io.File;
+import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.channels.FileChannel;
+import java.nio.channels.FileLock;
 
 import net.sourceforge.jnlp.runtime.JNLPRuntime;
 
@@ -292,4 +296,42 @@
 
     }
 
+    /**
+     * This will return a lock to the file specified.
+     * 
+     * @param path File path to file we want to lock.
+     * @param shared Specify if the lock will be a shared lock.
+     * @param allowBlock Specify if we should block when we can not get the
+     *            lock. Getting a shared lock will always block.
+     * @return FileLock if we were successful in getting a lock, otherwise null.
+     * @throws FileNotFoundException If the file does not exist.
+     */
+    public static FileLock getFileLock(String path, boolean shared, boolean allowBlock) throws FileNotFoundException {
+        RandomAccessFile rafFile = new RandomAccessFile(path, "rw");
+        FileChannel fc = rafFile.getChannel();
+        FileLock lock = null;
+        try {
+            if (!shared) {
+                if (allowBlock) {
+                    lock = fc.lock(0, Long.MAX_VALUE, false);
+                } else {
+                    lock = fc.tryLock(0, Long.MAX_VALUE, false);
+                }
+            } else { // We want shared lock. This will block regardless if allowBlock is true or not.
+                // Test to see if we can get a shared lock.
+                lock = fc.lock(0, 1, true); // Block if a non exclusive lock is being held.
+                if (!lock.isShared()) { // This lock is an exclusive lock. Use alternate solution.
+                    FileLock tempLock = null;
+                    for (long pos = 1; tempLock == null && pos < Long.MAX_VALUE - 1; pos++) {
+                        tempLock = fc.tryLock(pos, 1, false);
+                    }
+                    lock.release();
+                    lock = tempLock; // Get the unique exclusive lock.
+                }
+            }
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        return lock;
+    }
 }
--- a/netx/net/sourceforge/jnlp/util/XDesktopEntry.java	Mon Apr 18 15:13:23 2011 -0400
+++ b/netx/net/sourceforge/jnlp/util/XDesktopEntry.java	Mon Apr 18 17:38:31 2011 -0400
@@ -74,7 +74,7 @@
 
         String cacheDir = JNLPRuntime.getConfiguration()
                 .getProperty(DeploymentConfiguration.KEY_USER_CACHE_DIR);
-        File cacheFile = CacheUtil.urlToPath(file.getSourceLocation(), cacheDir);
+        File cacheFile = CacheUtil.getCacheFile(file.getSourceLocation(), null);
 
         String fileContents = "[Desktop Entry]\n";
         fileContents += "Version=1.0\n";