aboutsummaryrefslogtreecommitdiffstats
path: root/netx/net/sourceforge/jnlp/cache/ResourceTracker.java
diff options
context:
space:
mode:
Diffstat (limited to 'netx/net/sourceforge/jnlp/cache/ResourceTracker.java')
-rw-r--r--netx/net/sourceforge/jnlp/cache/ResourceTracker.java1049
1 files changed, 1049 insertions, 0 deletions
diff --git a/netx/net/sourceforge/jnlp/cache/ResourceTracker.java b/netx/net/sourceforge/jnlp/cache/ResourceTracker.java
new file mode 100644
index 0000000..d65c840
--- /dev/null
+++ b/netx/net/sourceforge/jnlp/cache/ResourceTracker.java
@@ -0,0 +1,1049 @@
+// Copyright (C) 2001-2003 Jon A. Maxwell (JAM)
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2.1 of the License, or (at your option) any later version.
+//
+// This library 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
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+
+package net.sourceforge.jnlp.cache;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.jar.JarOutputStream;
+import java.util.jar.Pack200;
+import java.util.jar.Pack200.Unpacker;
+import java.util.zip.GZIPInputStream;
+
+import net.sourceforge.jnlp.Version;
+import net.sourceforge.jnlp.event.DownloadEvent;
+import net.sourceforge.jnlp.event.DownloadListener;
+import net.sourceforge.jnlp.runtime.JNLPRuntime;
+import net.sourceforge.jnlp.util.WeakList;
+
+/**
+ * This class tracks the downloading of various resources of a
+ * JNLP file to local files in the cache. It can be used to
+ * download icons, jnlp and extension files, jars, and jardiff
+ * files using the version based protocol or any file using the
+ * basic download protocol (jardiff and version not implemented
+ * yet).<p>
+ *
+ * The resource tracker can be configured to prefetch resources,
+ * which are downloaded in the order added to the media
+ * tracker.<p>
+ *
+ * Multiple threads are used to download and cache resources that
+ * are actively being waited for (blocking a caller) or those that
+ * have been started downloading by calling the startDownload
+ * method. Resources that are prefetched are downloaded one at a
+ * time and only if no other trackers have requested downloads.
+ * This allows the tracker to start downloading many items without
+ * using many system resources, but still quickly download items
+ * as needed.<p>
+ *
+ * @author <a href="mailto:[email protected]">Jon A. Maxwell (JAM)</a> - initial author
+ * @version $Revision: 1.22 $
+ */
+public class ResourceTracker {
+
+ // todo: use event listener arrays instead of lists
+
+ // todo: see if there is a way to set the socket options just
+ // for use by the tracker so checks for updates don't hang for
+ // a long time.
+
+ // todo: ability to restart/retry a hung download
+
+ // todo: move resource downloading/processing code into Resource
+ // class, threading stays in ResourceTracker
+
+ // todo: get status method? and some way to convey error status
+ // to the caller.
+
+ // todo: might make a tracker be able to download more than one
+ // version of a resource, but probably not very useful.
+
+
+ // defines
+ // ResourceTracker.Downloader (download threads)
+
+ // separately locks on (in order of aquire order, ie, sync on prefetch never syncs on lock):
+ // lock, prefetch, this.resources, each resource, listeners
+
+ /** notified on initialization or download of a resource */
+ private static Object lock = new Integer(0); // used to lock static structures
+
+ // shortcuts
+ private static final int UNINITIALIZED = Resource.UNINITIALIZED;
+ private static final int CONNECT = Resource.CONNECT;
+ private static final int CONNECTING = Resource.CONNECTING;
+ private static final int CONNECTED = Resource.CONNECTED;
+ private static final int DOWNLOAD = Resource.DOWNLOAD;
+ private static final int DOWNLOADING = Resource.DOWNLOADING;
+ private static final int DOWNLOADED = Resource.DOWNLOADED;
+ private static final int ERROR = Resource.ERROR;
+ private static final int STARTED = Resource.STARTED;
+
+ /** max threads */
+ private static final int maxThreads = 5;
+
+ /** running threads */
+ private static int threads = 0;
+
+ /** weak list of resource trackers with resources to prefetch */
+ private static WeakList prefetchTrackers = new WeakList();
+
+ /** resources requested to be downloaded */
+ private static ArrayList queue = new ArrayList();
+
+ /** resource trackers threads are working for (used for load balancing across multi-tracker downloads) */
+ private static ArrayList active = new ArrayList(); //
+
+ /** the resources known about by this resource tracker */
+ private List resources = new ArrayList();
+
+ /** download listeners for this tracker */
+ private List listeners = new ArrayList();
+
+ /** whether to download parts before requested */
+ private boolean prefetch;
+
+
+ /**
+ * Creates a resource tracker that does not prefetch resources.
+ */
+ public ResourceTracker() {
+ this(false);
+ }
+
+ /**
+ * Creates a resource tracker.
+ *
+ * @param prefetch whether to download resources before requested.
+ */
+ public ResourceTracker(boolean prefetch) {
+ this.prefetch = prefetch;
+
+ if (prefetch) {
+ synchronized (prefetchTrackers) {
+ prefetchTrackers.add(this);
+ prefetchTrackers.trimToSize();
+ }
+ }
+ }
+
+ /**
+ * Add a resource identified by the specified location and
+ * version. The tracker only downloads one version of a given
+ * resource per instance (ie cannot download both versions 1 and
+ * 2 of a resource in the same tracker).
+ *
+ * @param location the location of the resource
+ * @param version the resource version
+ * @param updatePolicy whether to check for updates if already in cache
+ */
+ public void addResource(URL location, Version version, UpdatePolicy updatePolicy) {
+ if (location == null)
+ throw new IllegalArgumentException("location==null");
+
+ Resource resource = Resource.getResource(location, version, updatePolicy);
+ boolean downloaded = false;
+
+ synchronized (resources) {
+ if (resources.contains(resource))
+ return;
+ resource.addTracker(this);
+ resources.add(resource);
+ }
+
+ // checkCache may take a while (loads properties file). this
+ // should really be synchronized on resources, but the worst
+ // case should be that the resource will be updated once even
+ // if unnecessary.
+ downloaded = checkCache(resource, updatePolicy);
+
+ synchronized (lock) {
+ if (!downloaded)
+ if (prefetch && threads == 0) // existing threads do pre-fetch when queue empty
+ startThread();
+ }
+ }
+
+ /**
+ * Removes a resource from the tracker. This method is useful
+ * to allow memory to be reclaimed, but calling this method is
+ * not required as resources are reclaimed when the tracker is
+ * collected.
+ *
+ * @throws IllegalArgumentException if the resource is not being tracked
+ */
+ public void removeResource(URL location) {
+ synchronized (resources) {
+ Resource resource = getResource(location);
+
+ if (resource != null) {
+ resources.remove(resource);
+ resource.removeTracker(this);
+ }
+
+ // should remove from queue? probably doesn't matter
+ }
+ }
+
+ /**
+ * Check the cache for a resource, and initialize the resource
+ * as already downloaded if found. <p>
+ *
+ * @param updatePolicy whether to check for updates if already in cache
+ * @return whether the resource are already downloaded
+ */
+ private boolean checkCache(Resource resource, UpdatePolicy updatePolicy) {
+ if (!CacheUtil.isCacheable(resource.location, resource.downloadVersion)) {
+ // pretend that they are already downloaded; essentially
+ // they will just 'pass through' the tracker as if they were
+ // never added (for example, not affecting the total download size).
+ synchronized (resource) {
+ resource.changeStatus(0, DOWNLOADED|CONNECTED|STARTED);
+ }
+ fireDownloadEvent(resource);
+ return true;
+ }
+
+ if (updatePolicy != UpdatePolicy.ALWAYS && updatePolicy != UpdatePolicy.FORCE) { // save loading entry props file
+ CacheEntry entry = new CacheEntry(resource.location, resource.downloadVersion);
+
+ if (entry.isCached() && !updatePolicy.shouldUpdate(entry)) {
+ if (JNLPRuntime.isDebug())
+ System.out.println("not updating: "+resource.location);
+
+ synchronized (resource) {
+ resource.localFile = CacheUtil.getCacheFile(resource.location, resource.downloadVersion);
+ resource.size = resource.localFile.length();
+ resource.transferred = resource.localFile.length();
+ resource.changeStatus(0, DOWNLOADED|CONNECTED|STARTED);
+ }
+ fireDownloadEvent(resource);
+ return true;
+ }
+ }
+
+ if (updatePolicy == UpdatePolicy.FORCE) { // ALWAYS update
+ // When we are "always" updating, we update for each instance. Reset resource status.
+ resource.changeStatus(Integer.MAX_VALUE, 0);
+ }
+
+ // may or may not be cached, but check update when connection
+ // is open to possibly save network communication time if it
+ // has to be downloaded, and allow this call to return quickly
+ return false;
+ }
+
+ /**
+ * Adds the listener to the list of objects interested in
+ * receivind DownloadEvents.<p>
+ *
+ * @param location the resource to add a callback for
+ * @param runnable the runnable to call when resource is completed
+ */
+ public void addDownloadListener(DownloadListener listener) {
+ synchronized (listeners) {
+ if (!listeners.contains(listener))
+ listeners.add(listener);
+ }
+ }
+
+ /**
+ * Removes a download listener.
+ */
+ public void removeDownloadListener(DownloadListener listener) {
+ synchronized (listeners) {
+ listeners.remove(listener);
+ }
+ }
+
+ /**
+ * Fires the download event corresponding to the resource's
+ * state. This method is typicall called by the Resource itself
+ * on each tracker that is monitoring the resource. Do not call
+ * this method with any locks because the listeners may call
+ * back to this ResourceTracker.
+ */
+ protected void fireDownloadEvent(Resource resource) {
+ DownloadListener l[] = null;
+ synchronized (listeners) {
+ l = (DownloadListener[]) listeners.toArray(new DownloadListener[0]);
+ }
+
+ int status;
+ synchronized (resource) {
+ status = resource.status;
+ }
+
+ DownloadEvent event = new DownloadEvent(this, resource);
+ for (int i=0; i < l.length; i++) {
+ if (0 != ((ERROR|DOWNLOADED) & status))
+ l[i].downloadCompleted(event);
+ else if (0 != (DOWNLOADING & status))
+ l[i].downloadStarted(event);
+ else if (0 != (CONNECTING & status))
+ l[i].updateStarted(event);
+ }
+ }
+
+ /**
+ * Returns a URL pointing to the cached location of the
+ * resource, or the resource itself if it is a non-cacheable
+ * resource.<p>
+ *
+ * If the resource has not downloaded yet, the method will block
+ * until it has been transferred to the cache.<p>
+ *
+ * @param location the resource location
+ * @return the resource, or null if it could not be downloaded
+ * @throws IllegalArgumentException if the resource is not being tracked
+ * @see CacheUtil#isCacheable
+ */
+ public URL getCacheURL(URL location) {
+ try {
+ File f = getCacheFile(location);
+ if (f != null)
+ return f.toURL();
+ }
+ catch (MalformedURLException ex) {
+ if (JNLPRuntime.isDebug())
+ ex.printStackTrace();
+ }
+
+ return location;
+ }
+
+ /**
+ * Returns a file containing the downloaded resource. If the
+ * resource is non-cacheable then null is returned unless the
+ * resource is a local file (the original file is returned).<p>
+ *
+ * If the resource has not downloaded yet, the method will block
+ * until it has been transferred to the cache.<p>
+ *
+ * @param location the resource location
+ * @return a local file containing the resource, or null
+ * @throws IllegalArgumentException if the resource is not being tracked
+ * @see CacheUtil#isCacheable
+ */
+ public File getCacheFile(URL location) {
+ try {
+ Resource resource = getResource(location);
+ if (!resource.isSet(DOWNLOADED|ERROR))
+ waitForResource(location, 0);
+
+ if (resource.isSet(ERROR))
+ return null;
+
+ if (resource.localFile != null)
+ return resource.localFile;
+
+ if (location.getProtocol().equalsIgnoreCase("file")) {
+ File file = new File(location.getFile());
+ if (file.exists())
+ return file;
+ }
+
+ return null;
+ }
+ catch (InterruptedException ex) {
+ if (JNLPRuntime.isDebug())
+ ex.printStackTrace();
+
+ return null; // need an error exception to throw
+ }
+ }
+
+ /**
+ * Returns an input stream that reads the contents of the
+ * resource. For non-cacheable resources, an InputStream that
+ * reads from the source location is returned. Otherwise the
+ * InputStream reads the cached resource.<p>
+ *
+ * This method will block while the resource is downloaded to
+ * the cache.
+ *
+ * @throws IOException if there was an error opening the stream
+ * @throws IllegalArgumentException if the resource is not being tracked
+ */
+ public InputStream getInputStream(URL location) throws IOException {
+ try {
+ Resource resource = getResource(location);
+ if (!resource.isSet(DOWNLOADED|ERROR))
+ waitForResource(location, 0);
+
+ if (resource.localFile != null)
+ return new FileInputStream(resource.localFile);
+
+ return resource.location.openStream();
+ }
+ catch (InterruptedException ex) {
+ throw new IOException("wait was interrupted");
+ }
+ }
+
+ /**
+ * Wait for a group of resources to be downloaded and made
+ * available locally.
+ *
+ * @param urls the resources to wait for
+ * @param timeout the time in ms to wait before returning, 0 for no timeout
+ * @return whether the resources downloaded before the timeout
+ * @throws IllegalArgumentException if the resource is not being tracked
+ */
+ public boolean waitForResources(URL urls[], long timeout) throws InterruptedException {
+ Resource resources[] = new Resource[ urls.length ];
+
+ synchronized(resources) {
+ // keep the lock so getResource doesn't have to aquire it each time
+ for (int i=0; i < urls.length; i++)
+ resources[i] = getResource(urls[i]);
+ }
+
+ if (resources.length > 0)
+ return wait(resources, timeout);
+
+ return true;
+ }
+
+ /**
+ * Wait for a particular resource to be downloaded and made
+ * available.
+ *
+ * @param location the resource to wait for
+ * @param timeout the timeout, or 0 to wait until completed
+ * @return whether the resource downloaded before the timeout
+ * @throws InterruptedException if another thread interrupted the wait
+ * @throws IllegalArgumentException if the resource is not being tracked
+ */
+ public boolean waitForResource(URL location, long timeout) throws InterruptedException {
+ return wait(new Resource[] { getResource(location) }, timeout);
+ }
+
+ /**
+ * Returns the number of bytes downloaded for a resource.
+ *
+ * @param location the resource location
+ * @return the number of bytes transferred
+ * @throws IllegalArgumentException if the resource is not being tracked
+ */
+ public long getAmountRead(URL location) {
+ // not atomic b/c transferred is a long, but so what (each
+ // byte atomic? so probably won't affect anything...)
+ return getResource(location).transferred;
+ }
+
+ /**
+ * Returns whether a resource is available for use (ie, can be
+ * accessed with the getCacheFile method).
+ *
+ * @throws IllegalArgumentException if the resource is not being tracked
+ */
+ public boolean checkResource(URL location) {
+ return getResource(location).isSet(DOWNLOADED|ERROR); // isSet atomic
+ }
+
+ /**
+ * Starts loading the resource if it is not already being
+ * downloaded or already cached. Resources started downloading
+ * using this method may download faster than those prefetched
+ * by the tracker because the tracker will only prefetch one
+ * resource at a time to conserve system resources.
+ *
+ * @return true if the resource is already downloaded (or an error occurred)
+ * @throws IllegalArgumentException if the resource is not being tracked
+ */
+ public boolean startResource(URL location) {
+ Resource resource = getResource(location);
+
+ return startResource(resource);
+ }
+
+ /**
+ * Sets the resource status to connect and download, and
+ * enqueues the resource if not already started.
+ *
+ * @return true if the resource is already downloaded (or an error occurred)
+ * @throws IllegalArgumentException if the resource is not being tracked
+ */
+ private boolean startResource(Resource resource) {
+ boolean enqueue = false;
+
+ synchronized (resource) {
+ if (resource.isSet(ERROR))
+ return true;
+
+ enqueue = !resource.isSet(STARTED);
+
+ if (!resource.isSet(CONNECTED | CONNECTING))
+ resource.changeStatus(0, CONNECT|STARTED);
+ if (!resource.isSet(DOWNLOADED | DOWNLOADING))
+ resource.changeStatus(0, DOWNLOAD|STARTED);
+
+ if (!resource.isSet(DOWNLOAD|CONNECT))
+ enqueue = false;
+ }
+
+ if (enqueue)
+ queueResource(resource);
+
+ return !enqueue;
+ }
+
+ /**
+ * Returns the number of total size in bytes of a resource, or
+ * -1 it the size is not known.
+ *
+ * @param location the resource location
+ * @return the number of bytes, or -1
+ * @throws IllegalArgumentException if the resource is not being tracked
+ */
+ public long getTotalSize(URL location) {
+ return getResource(location).size; // atomic
+ }
+
+ /**
+ * Start a new download thread if there are not too many threads
+ * already running.<p>
+ *
+ * Calls to this method should be synchronized on lock.
+ */
+ protected void startThread() {
+ if (threads < maxThreads) {
+ threads++;
+
+ Thread thread = new Thread(new Downloader());
+ thread.start();
+ }
+ }
+
+ /**
+ * A thread is ending, called by the thread itself.<p>
+ *
+ * Calls to this method should be synchronized.
+ */
+ private void endThread() {
+ threads--;
+
+ if (threads < 0) {
+ // this should never happen but try to recover
+ threads = 0;
+
+ if (queue.size() > 0) // if any on queue make sure a thread is running
+ startThread(); // look into whether this could create a loop
+
+ throw new RuntimeException("tracker threads < 0");
+ }
+
+ if (threads == 0) {
+ synchronized (prefetchTrackers) {
+ queue.trimToSize(); // these only accessed by threads so no sync needed
+ active.clear(); // no threads so no trackers actively downloading
+ active.trimToSize();
+ prefetchTrackers.trimToSize();
+ }
+ }
+ }
+
+ /**
+ * Add a resource to the queue and start a thread to download or
+ * initialize it.
+ */
+ private void queueResource(Resource resource) {
+ synchronized (lock) {
+ if (!resource.isSet(CONNECT|DOWNLOAD))
+ throw new IllegalArgumentException("Invalid resource state (resource: "+resource+")");
+
+ queue.add(resource);
+ startThread();
+ }
+ }
+
+ /**
+ * Process the resource by either downloading it or initializing
+ * it.
+ */
+ private void processResource(Resource resource) {
+ boolean doConnect = false;
+ boolean doDownload = false;
+
+ synchronized (resource) {
+ if (resource.isSet(CONNECTING))
+ doConnect = true;
+ }
+ if (doConnect)
+ initializeResource(resource);
+
+ synchronized (resource) {
+ // return to queue if we just initalized but it still needs
+ // to download (not cached locally / out of date)
+ if (resource.isSet(DOWNLOAD)) // would be DOWNLOADING if connected before this method
+ queueResource(resource);
+
+ if (resource.isSet(DOWNLOADING))
+ doDownload = true;
+ }
+ if (doDownload)
+ downloadResource(resource);
+ }
+
+ /**
+ * Downloads a resource to a file, uncompressing it if required
+ *
+ * @param resource the resource to download
+ */
+ private void downloadResource(Resource resource) {
+ resource.fireDownloadEvent(); // fire DOWNLOADING
+
+ try {
+ // create out second in case in does not exist
+ URLConnection con = getVersionedResourceURL(resource).openConnection();
+ con.addRequestProperty("Accept-Encoding", "pack200-gzip, gzip");
+
+ con.connect();
+
+ /*
+ * We dont really know what we are downloading. If we ask for
+ * foo.jar, the server might send us foo.jar.pack.gz or foo.jar.gz
+ * instead. So we save the file with the appropriate extension
+ */
+ URL downloadLocation = resource.location;
+
+ String contentEncoding = con.getContentEncoding();
+
+ if (JNLPRuntime.isDebug()) {
+ System.err.println("Content encoding for " + resource.location + ": "
+ + contentEncoding);
+ }
+
+ if (contentEncoding != null) {
+ if (contentEncoding.equals("gzip")) {
+ downloadLocation = new URL(downloadLocation.toString() + ".gz");
+ } else if (contentEncoding.equals("pack200-gzip")) {
+ downloadLocation = new URL(downloadLocation.toString() + ".pack.gz");
+ }
+ }
+
+ InputStream in = new BufferedInputStream(con.getInputStream());
+ OutputStream out = CacheUtil.getOutputStream(downloadLocation, resource.downloadVersion);
+ byte buf[] = new byte[1024];
+ int rlen;
+
+ 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();
+
+ /*
+ * If the file was compressed, uncompress it.
+ */
+ if (contentEncoding != null) {
+ if (contentEncoding.equals("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 if (contentEncoding.equals("pack200-gzip")) {
+ 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();
+ }
+ }
+
+ resource.changeStatus(DOWNLOADING, DOWNLOADED);
+ synchronized(lock) {
+ lock.notifyAll(); // wake up wait's to check for completion
+ }
+ resource.fireDownloadEvent(); // fire DOWNLOADED
+ }
+ catch (Exception ex) {
+ if (JNLPRuntime.isDebug())
+ ex.printStackTrace();
+
+ resource.changeStatus(0, ERROR);
+ synchronized(lock) {
+ lock.notifyAll(); // wake up wait's to check for completion
+ }
+ resource.fireDownloadEvent(); // fire ERROR
+ }
+ }
+
+ /**
+ * Open a URL connection and get the content length and other
+ * fields.
+ */
+ private void initializeResource(Resource resource) {
+ resource.fireDownloadEvent(); // fire CONNECTING
+
+ try {
+ File localFile = CacheUtil.getCacheFile(resource.location, resource.downloadVersion);
+
+ // connect
+ URLConnection connection = getVersionedResourceURL(resource).openConnection(); // this won't change so should be okay unsynchronized
+ connection.addRequestProperty("Accept-Encoding", "pack200-gzip, gzip");
+
+ int size = connection.getContentLength();
+ boolean current = CacheUtil.isCurrent(resource.location, resource.requestVersion, connection) && resource.getUpdatePolicy() != UpdatePolicy.FORCE;
+
+ synchronized(resource) {
+ resource.localFile = localFile;
+ // resource.connection = connection;
+ resource.size = size;
+ resource.changeStatus(CONNECT|CONNECTING, CONNECTED);
+
+ // check if up-to-date; if so set as downloaded
+ if (current)
+ resource.changeStatus(DOWNLOAD|DOWNLOADING, DOWNLOADED);
+ }
+
+ // update cache entry
+ CacheEntry entry = new CacheEntry(resource.location, resource.requestVersion);
+ if (!current)
+ entry.initialize(connection);
+
+ entry.setLastUpdated(System.currentTimeMillis());
+ entry.store();
+
+ synchronized(lock) {
+ lock.notifyAll(); // wake up wait's to check for completion
+ }
+ resource.fireDownloadEvent(); // fire CONNECTED
+
+ // explicitly close the URLConnection.
+ if (connection instanceof HttpURLConnection)
+ ((HttpURLConnection)connection).disconnect();
+ }
+ catch (Exception ex) {
+ if (JNLPRuntime.isDebug())
+ ex.printStackTrace();
+
+ resource.changeStatus(0, ERROR);
+ synchronized(lock) {
+ lock.notifyAll(); // wake up wait's to check for completion
+ }
+ resource.fireDownloadEvent(); // fire ERROR
+ }
+ }
+
+ /**
+ * Returns the versioned url for a resource
+ * @param resource the resource to get the url for
+ */
+ private URL getVersionedResourceURL(Resource resource) {
+ String actualLocation = resource.location.getProtocol() + "://"
+ + resource.location.getHost();
+ if (resource.location.getPort() != -1) {
+ actualLocation += ":" + resource.location.getPort();
+ }
+ actualLocation += resource.location.getPath();
+ if (resource.requestVersion != null
+ && resource.requestVersion.isVersionId()) {
+ actualLocation += "?version-id=" + resource.requestVersion;
+ }
+ URL versionedURL;
+ try {
+ versionedURL = new URL(actualLocation);
+ } catch (MalformedURLException e) {
+ return resource.location;
+ }
+ return versionedURL;
+ }
+
+
+ /**
+ * Pick the next resource to download or initialize. If there
+ * are no more resources requested then one is taken from a
+ * resource tracker with prefetch enabled.<p>
+ *
+ * The resource state is advanced before it is returned
+ * (CONNECT-&gt;CONNECTING).<p>
+ *
+ * Calls to this method should be synchronized on lock.<p>
+ *
+ * @return the resource to initialize or download, or null
+ */
+ private static Resource selectNextResource() {
+ Resource result;
+
+ // pick from queue
+ result = selectByFlag(queue, CONNECT, ERROR); // connect but not error
+ if (result == null)
+ result = selectByFlag(queue, DOWNLOAD, ERROR|CONNECT|CONNECTING);
+
+ // remove from queue if found
+ if (result != null)
+ queue.remove(result);
+
+ // prefetch if nothing found so far and this is the last thread
+ if (result == null && threads == 1)
+ result = getPrefetch();
+
+ if (result == null)
+ return null;
+
+ synchronized (result) {
+ if (result.isSet(CONNECT)) {
+ result.changeStatus(CONNECT, CONNECTING);
+ }
+ else if (result.isSet(DOWNLOAD)) {
+ // only download if *not* connecting, when done connecting
+ // select next will pick up the download part. This makes
+ // all requested connects happen before any downloads, so
+ // the size is known as early as possible.
+ result.changeStatus(DOWNLOAD, DOWNLOADING);
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Returns the next resource to be prefetched before
+ * requested.<p>
+ *
+ * Calls to this method should be synchronized on lock.<p>
+ */
+ private static Resource getPrefetch() {
+ Resource result = null;
+ Resource alternate = null;
+
+ // first find one to initialize
+ synchronized (prefetchTrackers) {
+ for (int i=0; i < prefetchTrackers.size() && result == null; i++) {
+ ResourceTracker tracker = (ResourceTracker) prefetchTrackers.get(i);
+ if (tracker == null)
+ continue;
+
+ synchronized (tracker.resources) {
+ result = selectByFlag(tracker.resources, UNINITIALIZED, ERROR);
+
+ if (result == null && alternate == null)
+ alternate = selectByFlag(tracker.resources, CONNECTED, ERROR|DOWNLOADED|DOWNLOADING|DOWNLOAD);
+ }
+ }
+ }
+
+ // if none to initialize, switch to download
+ if (result == null)
+ result = alternate;
+
+ if (result == null)
+ return null;
+
+ synchronized (result) {
+ ResourceTracker tracker = result.getTracker();
+ if (tracker == null)
+ return null; // GC of tracker happened between above code and here
+
+ // prevents startResource from putting it on queue since
+ // we're going to return it.
+ result.changeStatus(0, STARTED);
+
+ tracker.startResource(result);
+ }
+
+ return result;
+ }
+
+ /**
+ * Selects a resource from the source list that has the
+ * specified flag set.<p>
+ *
+ * Calls to this method should be synchronized on lock and
+ * source list.<p>
+ */
+ private static Resource selectByFlag(List source, int flag, int notflag) {
+ Resource result = null;
+ int score = Integer.MAX_VALUE;
+
+ for (int i=0; i < source.size(); i++) {
+ Resource resource = (Resource) source.get(i);
+ boolean selectable = false;
+
+ synchronized (resource) {
+ if (resource.isSet(flag) && !resource.isSet(notflag))
+ selectable = true;
+ }
+
+ if (selectable) {
+ int activeCount = 0;
+
+ for (int j=0; j < active.size(); j++)
+ if ((ResourceTracker)active.get(j) == resource.getTracker())
+ activeCount++;
+
+ // try to spread out the downloads so that a slow host
+ // won't monopolize the downloads
+ if (activeCount < score) {
+ result = resource;
+ score = activeCount;
+ }
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Return the resource matching the specified URL.
+ *
+ * @throws IllegalArgumentException if the resource is not being tracked
+ */
+ private Resource getResource(URL location) {
+ synchronized (resources) {
+ for (int i=0; i < resources.size(); i++) {
+ Resource resource = (Resource) resources.get(i);
+
+ if (CacheUtil.urlEquals(resource.location, location))
+ return resource;
+ }
+ }
+
+ throw new IllegalArgumentException("Location does not specify a resource being tracked.");
+ }
+
+ /**
+ * Wait for some resources.
+ *
+ * @param resources the resources to wait for
+ * @param timeout the timeout, or 0 to wait until completed
+ * @returns true if the resources were downloaded or had errors,
+ * false if the timeout was reached
+ * @throws InterruptedException if another thread interrupted the wait
+ */
+ private boolean wait(Resource resources[], long timeout) throws InterruptedException {
+ long startTime = System.currentTimeMillis();
+
+ // start them downloading / connecting in background
+ for (int i=0; i < resources.length; i++)
+ startResource(resources[i]);
+
+ // wait for completion
+ while (true) {
+ boolean finished = true;
+
+ synchronized (lock) {
+ // check for completion
+ for (int i=0; i < resources.length; i++) {
+ //NetX Deadlocking may be solved by removing this
+ //synch block.
+ synchronized (resources[i]) {
+ if (!resources[i].isSet(DOWNLOADED | ERROR)) {
+ finished = false;
+ break;
+ }
+ }
+ }
+ if (finished)
+ return true;
+
+ // wait
+ long waitTime = 0;
+
+ if (timeout > 0) {
+ waitTime = timeout - (System.currentTimeMillis()-startTime);
+ if (waitTime <= 0)
+ return false;
+ }
+
+ lock.wait(waitTime);
+ }
+ }
+ }
+
+
+ // inner classes
+
+ /**
+ * This class downloads and initializes the queued resources.
+ */
+ class Downloader implements Runnable {
+ Resource resource = null;
+
+ public void run() {
+ while (true) {
+ synchronized (lock) {
+ // remove from active list, used for load balancing
+ if (resource != null)
+ active.remove(resource.getTracker());
+
+ resource = selectNextResource();
+
+ if (resource == null) {
+ endThread();
+ break;
+ }
+
+ // add to active list, used for load balancing
+ active.add(resource.getTracker());
+ }
+
+ try {
+ processResource(resource);
+ }
+ catch (Exception ex) {
+ if (JNLPRuntime.isDebug())
+ ex.printStackTrace();
+ }
+ }
+ // should have a finally in case some exception is thrown by
+ // selectNextResource();
+ }
+ };
+
+}