diff options
Diffstat (limited to 'netx/net/sourceforge/jnlp/cache/ResourceTracker.java')
-rw-r--r-- | netx/net/sourceforge/jnlp/cache/ResourceTracker.java | 1049 |
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->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(); + } + }; + +} |