/*
* The Sun Project JXTA(TM) Software License
*
* Copyright (c) 2001-2007 Sun Microsystems, Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. The end-user documentation included with the redistribution, if any, must
* include the following acknowledgment: "This product includes software
* developed by Sun Microsystems, Inc. for JXTA(TM) technology."
* Alternately, this acknowledgment may appear in the software itself, if
* and wherever such third-party acknowledgments normally appear.
*
* 4. The names "Sun", "Sun Microsystems, Inc.", "JXTA" and "Project JXTA" must
* not be used to endorse or promote products derived from this software
* without prior written permission. For written permission, please contact
* Project JXTA at http://www.jxta.org.
*
* 5. Products derived from this software may not be called "JXTA", nor may
* "JXTA" appear in their name, without prior written permission of Sun.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SUN
* MICROSYSTEMS OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* JXTA is a registered trademark of Sun Microsystems, Inc. in the United
* States and other countries.
*
* Please see the license information page at :
* <http://www.jxta.org/project/www/license.html> for instructions on use of
* the license in source files.
*
* ====================================================================
* This software consists of voluntary contributions made by many individuals
* on behalf of Project JXTA. For more information on Project JXTA, please see
* http://www.jxta.org.
*
* This license is based on the BSD license adopted by the Apache Foundation.
*/
package net.jxta.impl.content;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import net.jxta.logging.Logger;
import net.jxta.logging.Logging;
import net.jxta.peergroup.PeerGroup;
import net.jxta.content.Content;
import net.jxta.content.ContentID;
import net.jxta.content.ContentProvider;
import net.jxta.content.ContentSourceLocationState;
import net.jxta.content.ContentTransfer;
import net.jxta.content.ContentTransferEvent;
import net.jxta.content.ContentTransferListener;
import net.jxta.content.ContentTransferState;
import net.jxta.content.TransferCancelledException;
import net.jxta.content.TransferException;
import net.jxta.content.TransferFailedException;
import net.jxta.discovery.DiscoveryEvent;
import net.jxta.discovery.DiscoveryListener;
import net.jxta.discovery.DiscoveryService;
import net.jxta.protocol.ContentShareAdvertisement;
/**
* Abstract implementation of a ContentProvider, providing the basic
* infrastructure for locating the desired quantity of remote advertisements,
* notifying listeners of location state changes, and basic infrastrucure
* around transfer attempts. This class is by no means required to be
* extended to implement a ContentTransfer but simply attempts to consolidate
* much of the logic that would be repeated throughout many implementations.
*/
public abstract class AbstractContentTransfer
implements ContentTransfer {
/**
* The default source location interval, in seconds. This constant
* is provided for information purposes only - please do not use.
*/
protected static final int DEFAULT_SOURCE_LOCATION_INTERVAL = 15;
/**
* The default number of advertisements that will be requested from
* the discovery service when attempting to discover more source
* locations. This constant is provided for information purposes
* only - please do not use.
*/
protected static final int DEFAULT_DISCOVERY_THRESHOLD = 10;
/**
* The default number of consecutive transfer attempts that report
* stalled status before the transfer provider will be de-selected.
*/
protected static final int DEFAULT_MAX_STALLS = 3;
/**
* The default value used to determine if local advertisements
* will be searched for when looking for new source locations.
* This constant is provided for information purposes only - please
* do not use.
*/
protected static final boolean DEFAULT_ENABLE_LOCAL = true;
/**
* The default value used to determine if remote advertisements
* will be searched for when looking for new source locations.
* This constant is provided for information purposes only - please
* do not use.
*/
protected static final boolean DEFAULT_ENABLE_REMOTE = true;
/**
* Logger instance.
*/
private final Logger LOG;
/**
* List of our listeners.
*/
private final CopyOnWriteArrayList<ContentTransferListener> listeners =
new CopyOnWriteArrayList<ContentTransferListener>();
/**
* Executor service that we will use to schedule tasks.
*/
private final ScheduledExecutorService executor;
/**
* Our parent peer group.
*/
private final PeerGroup peerGroup;
/**
* ContentProvider which created and manages this transfer.
*/
private final ContentProvider provider;
/**
* The master content ID that we are using to identify
* our intended content to retrieve.
*/
private final ContentID masterID;
/**
* DiscoveryListener interface to use during source location.
*/
private final DiscoListener discoveryListener = new DiscoListener();
/**
* Runnable to use when creating new location tasks.
*/
private final Runnable locationRunnable = new Runnable() {
public void run() {
try {
locationExecution();
} catch (RuntimeException rtx) {
Logging.logCheckedWarning(LOG, "Uncaught exception in location execution\n", rtx);
}
}
};
/**
* Runnable to use when creating new transfer tasks.
*/
private final Runnable transferRunnable = new Runnable() {
public void run() {
try {
transferExecution();
} catch (InterruptedException intx) {
Thread.interrupted();
Logging.logCheckedDebug(LOG, "Transfer thread interrupted\n", intx);
}
}
};
/**
* Object used to lock, wait, and notify on internally.
*/
private final Object lockObject = this;
/**
* The number of seconds between source discovery attempts.
*/
private int sourceLocationInterval = DEFAULT_SOURCE_LOCATION_INTERVAL;
/**
* The number of of allSources considered to be "enough".
*/
private int enoughSources = getEnoughLocationCount();
/**
* The number of of allSources considered to be "many".
*/
private int manySources = getManyLocationCount();
/**
* The discovery threshold to use.
*/
private int discoveryThreshold = DEFAULT_DISCOVERY_THRESHOLD;
/**
* Flag indicating whether or not local advertisements will be queried
* when searching for data allSources. Used for debug.
*/
private boolean allowLocalDiscovery = DEFAULT_ENABLE_LOCAL;
/**
* Flag indicating whether or not remote advertisements will be queried
* when searching for data allSources. Used for debug.
*/
private boolean allowRemoteDiscovery = DEFAULT_ENABLE_REMOTE;
/**
* Maximum consecutive times a transfer attempt can report stalled
* before the provider is de-selected.
*/
private int maxStalls = DEFAULT_MAX_STALLS;
/**
* Current location state.
*/
private ContentSourceLocationState locationState
= ContentSourceLocationState.NOT_LOCATING;
/**
* Current transfer state.
*/
private ContentTransferState transferState =
ContentTransferState.PENDING;
// Managed over the course of the transfer
/**
* Transfer state when the last check was performed. This allows
* us to detect changes which require event notification.
*/
private ContentTransferState lastTransferState = transferState;
/**
* Sources that we know about that we have not yet tried to retrieve
* from.
*/
private List<ContentShareAdvertisement> allSources =
new CopyOnWriteArrayList<ContentShareAdvertisement>();
/**
* Sources that we know about but which we've marked as not being
* usable.
*/
private List<ContentShareAdvertisement> uselessSources =
new CopyOnWriteArrayList<ContentShareAdvertisement>();
/**
* Sources which have been found since the last transfer attempt.
*/
private List<ContentShareAdvertisement> newSources =
new ArrayList<ContentShareAdvertisement>();
/**
* Future used when scheduling source location activity.
*/
private ScheduledFuture locationTask = null;
/**
* Future used when scheduling transfer activity.
*/
private ScheduledFuture transferTask = null;
/**
* The destination file that the content data should be dumped into
* when transferring.
*/
private File destFile;
/**
* Once the transfer has completed successfully, a reference to the
* Content object that was created.
*/
private Content content;
/**
* Transfer state goal.
*/
private ContentTransferState goalState =
ContentTransferState.PENDING;
/**
* Exception caught during transfer. This is used to hold the exception
* for use in wait blocks.
*/
private TransferException reason;
//////////////////////////////////////////////////////////////////////////
// Inner classes:
/**
* This class is used to hide our discovery listener interface from the
* rest of the world.
*/
private class DiscoListener implements DiscoveryListener {
/**
* {@inheritDoc}
*/
public void discoveryEvent(DiscoveryEvent event) {
addDiscoveredSources(event.getSearchResults());
}
}
//////////////////////////////////////////////////////////////////////////
// Constructors:
/**
* Contructor.
*
* @param origin content provider which created and manager this
* transfer
* @param schedExecutor scheduled executor to use when scheduling source
* location activities.
* @param group peer group that this transfer is taking place within
* @param contentAdv master advertisement used to identify the desired
* content
* @param loggerID logger name under which to log messages
*/
protected AbstractContentTransfer(
ContentProvider origin,
ScheduledExecutorService schedExecutor,
PeerGroup group,
ContentShareAdvertisement contentAdv,
String loggerID) {
provider = origin;
executor = schedExecutor;
peerGroup = group;
masterID = contentAdv.getContentID();
LOG = Logging.getLogger(
AbstractContentTransfer.class.getName() + "-" + loggerID);
addDiscoveredSource(contentAdv);
}
/**
* Contructor.
*
* @param origin content provider which created and manager this
* transfer
* @param schedExecutor scheduled executor to use when scheduling source
* location activities.
* @param group peer group that this transfer is taking place within
* @param contentID ID of the content that we would like to retrieve
* @param loggerID logger name under which to log messages
*/
protected AbstractContentTransfer(
ContentProvider origin,
ScheduledExecutorService schedExecutor,
PeerGroup group,
ContentID contentID,
String loggerID) {
provider = origin;
executor = schedExecutor;
peerGroup = group;
masterID = contentID;
LOG = Logging.getLogger(
AbstractContentTransfer.class.getName() + "-" + loggerID);
}
//////////////////////////////////////////////////////////////////////////
// ContentTransfer interface methods:
/**
* {@inheritDoc}
*/
public void addContentTransferListener(
ContentTransferListener listener) {
listeners.add(listener);
}
/**
* {@inheritDoc}
*/
public void removeContentTransferListener(
ContentTransferListener listener) {
listeners.remove(listener);
}
/**
* {@inheritDoc}
*/
public ContentProvider getContentProvider() {
return provider;
}
/**
* {@inheritDoc}
*/
public void startSourceLocation() {
boolean doFire = false;
synchronized(lockObject) {
if (!locationState.isLocating() && !locationState.hasMany()) {
// Start locating
doFire = true;
locationState = locationState.getEquivalent(true);
Logging.logCheckedDebug(LOG, "Starting source location (state=",
locationState, ", interval=",
sourceLocationInterval, ", locationTask=",
locationTask, ")");
if (locationTask == null || locationTask.isDone()) {
locationTask = executor.scheduleWithFixedDelay(
locationRunnable, 0, sourceLocationInterval,
TimeUnit.SECONDS);
}
}
}
if (doFire) {
fireLocationStateChanged();
}
}
/**
* {@inheritDoc}
*/
public void stopSourceLocation() {
boolean doFire = false;
synchronized(lockObject) {
if (locationState.isLocating()) {
// Stop locating
locationState = locationState.getEquivalent(false);
locationTask.cancel(false);
locationTask = null;
doFire = true;
Logging.logCheckedDebug(LOG, "Stopping source location (state=", locationState, ")");
}
}
if (doFire) {
fireLocationStateChanged();
}
}
/**
* {@inheritDoc}
*/
public ContentSourceLocationState getSourceLocationState() {
synchronized(lockObject) {
return locationState;
}
}
/**
* {@inheritDoc}
*/
public void startTransfer(File destination) {
if (destination == null) {
throw(new IllegalArgumentException(
"Destination cannot be null"));
} else if (destFile != null) {
throw(new IllegalStateException(
"This transfer has already been started"));
}
synchronized(lockObject) {
if (goalState.isFinished() || goalState.isRetrieving()) {
// Ignore.
return;
}
goalState = ContentTransferState.RETRIEVING;
destFile = destination;
}
startSourceLocation();
checkTransferState();
}
/**
* {@inheritDoc}
*/
public ContentTransferState getTransferState() {
synchronized(lockObject) {
return transferState;
}
}
/**
* {@inheritDoc}
*/
public void cancel() {
synchronized(lockObject) {
goalState = ContentTransferState.CANCELLED;
if (reason == null) {
reason = new TransferCancelledException();
}
}
checkTransferState();
}
/**
* {@inheritDoc}
*/
public void waitFor()
throws InterruptedException, TransferException {
waitFor(0);
}
/**
* {@inheritDoc}
*/
public void waitFor(long timeout)
throws InterruptedException, TransferException {
synchronized(lockObject) {
if (transferState.isFinished()) {
if (transferState.isSuccessful()) {
return;
} else {
throw(reason);
}
}
lockObject.wait(timeout);
}
}
/**
* {@inheritDoc}
*/
public Content getContent() throws InterruptedException, TransferException {
synchronized(lockObject) {
waitFor();
// If we get here, we're successful
return content;
}
}
//////////////////////////////////////////////////////////////////////////
// Protected and abstract methods:
/**
* Gets the ContentID which we are attempting to transfer.
*
* @return ContentID provided either directly or indirectly through
* one of the constructors.
*/
protected ContentID getTransferContentID() {
return masterID;
}
/**
* Sets the resulting Content object. This method should be called as
* during a transferAttempt implementation execution run, prior to that
* execution run returning ContentTransferState.COMPLETED.
*
* @param finalContent successfully retrieved content instance
*/
protected void setContent(Content finalContent) {
if (finalContent == null) {
Logging.logCheckedDebug(LOG, "Attempt to set null Content");
// Ignore
return;
}
if (!finalContent.getContentID().equals(masterID)) {
Logging.logCheckedDebug(LOG, "Attempt to set Content with wrong ID: ",
finalContent.getContentID());
// Ignore
return;
}
synchronized(lockObject) {
if (content == null) {
Logging.logCheckedDebug(LOG, "Setting Content to: ", finalContent);
content = finalContent;
lockObject.notifyAll();
}
}
}
/**
* Gets the list of ContentTransferListeners associated with this
* transfer instance. This list is safe to iterate over and modify,
* as it is backed by a copy-on-write implementation.
*
* @return list of ContentTransferListeners
*/
protected List<ContentTransferListener> getContentTransferListeners() {
return listeners;
}
/**
* Sets the source location interval that will be used when scheduling
* new discovery requests when attempting to locate more source locations.
*
* @param newInterval new source location interval, in seconds
*/
protected void setSourceLocationInterval(int newInterval) {
if (newInterval <= 0) {
throw(new IllegalArgumentException(
"New interval must be > 0 (was: " + newInterval + ")"));
}
synchronized(lockObject) {
sourceLocationInterval = newInterval;
}
}
/**
* Sets the discovery threshold used to limit the number of responses
* that will be returned by the discovery service when attempting to
* locate more data allSources.
*
* @param newThreshold the new maximum number of discovery responses to
* request from the discovery service
*/
protected void setDiscoveryThreshold(int newThreshold) {
if (newThreshold <= 0) {
throw(new IllegalArgumentException(
"New threshold must be > 0 (was: " + newThreshold + ")"));
}
synchronized(lockObject) {
discoveryThreshold = newThreshold;
}
}
/**
* Sets the flag indicating whether or not local advertisements will be
* search for when attempting to locate new source locations. This is
* primarily intended for debug and is safe to leave enabled.
*
* @param enabled true to search local advertisements, false to not
*/
protected void setLocalDiscoveryEnabled(boolean enabled) {
synchronized(lockObject) {
allowLocalDiscovery = enabled;
}
}
/**
* Sets the flag indicating whether or not remote advertisements will be
* search for when attempting to locate new source locations. This is
* primarily intended for debug and is safe to leave enabled.
*
* @param enabled true to search remote advertisements, false to not
*/
protected void setRemoteDiscoveryEnabled(boolean enabled) {
synchronized(lockObject) {
allowRemoteDiscovery = enabled;
}
}
/**
* Gets the number of source location advertisements that must be
* obtained before attempting to start a transfer. Typically, this
* will be a small number, even 1.
*
* @return number of advertisements to discover before transfer will
* be attempted
*/
protected abstract int getEnoughLocationCount();
/**
* Gets the number of source location advertisements that must be
* obtained before source location will be shut down. This should
* represent some happy medium between having too few allSources to be
* efficient and having too many allSources to practically use.
*
* @return number of advertisements to discover before source location
* will be stopped
*/
protected abstract int getManyLocationCount();
/**
* Determines whether or not the advertisement provided is of use
* to the provider. Advertisements not of use are not counted toward
* the provider's "enough" and "many" thresholds.
*
* @param adv advertisement to test
* @return true if advertisment is of use, false otherwise
*/
protected abstract boolean isAdvertisementOfUse(ContentShareAdvertisement adv);
/**
* Attempt to transfer the content from the allSources provided, using the
* specified destination file for data storage.
*
* @param dest destination file which should be used to store the
* content data
* @param sources all currently known sources
* @param newSources all sources which have been discovered since the
* last transfer attempt
* @return content transfer state best representing the results of this
* attempt. Must be set to ContentState.COMPLETED on success, in which
* case the implementation must have called setContent().
* @throws TransferFailedException when the transfer has irrecoverably
* failed. No more transfer attempts will be made and the failure will
* be propagated to the user.
* @throws TransferException when the transfer has failed and it is likely
* that a retry (either now or in the near future) will have better
* success.
*/
protected abstract ContentTransferState transferAttempt(
File dest,
List<ContentShareAdvertisement> sources,
List<ContentShareAdvertisement> newSources)
throws TransferException;
//////////////////////////////////////////////////////////////////////////
// Private methods:
/**
* Checks to see if the transfer state has changed or if any action
* needs to be taked to keep the transfer progressing. In general, this
* method is responsible for tracking and keeping the transfer state
* healthy.
*/
private void checkTransferState() {
boolean doFire = false;
boolean doNotify = false;
boolean doInterrupt = false;
boolean doStopLocation = false;
synchronized(lockObject) {
if (Logging.SHOW_DEBUG && LOG.isDebugEnabled()) {
Logging.logCheckedDebug(LOG, "Check transfer state");
Logging.logCheckedDebug(LOG, " Last State: ", lastTransferState);
Logging.logCheckedDebug(LOG, " State : ", transferState);
Logging.logCheckedDebug(LOG, " Goal : ", goalState);
}
if (lastTransferState != transferState) doFire = true;
if (goalState != transferState) {
switch(goalState) {
case RETRIEVING:
if (transferState.isSuccessful()) {
goalState = transferState;
doFire = true;
doNotify = true;
doStopLocation = true;
} else if (!transferState.isFinished()
&& !transferState.isRetrieving()) {
// Start retrieving
Logging.logCheckedDebug(LOG, "Setting up transfer execution");
transferState = goalState;
if (transferTask == null || transferTask.isDone()){
transferTask = executor.schedule(
transferRunnable, 0, TimeUnit.SECONDS);
}
doFire = true;
}
break;
case COMPLETED:
// Fall through
case CANCELLED:
// Fall through
case FAILED:
if (!transferState.isFinished()) {
transferState = goalState;
doFire = true;
doNotify = true;
doInterrupt = true;
doStopLocation = true;
}
break;
default:
throw(new IllegalStateException(
"Unhandled goal state: " + goalState
+ " - Please report!"));
}
if (lastTransferState != transferState) {
doFire = true;
}
}
lastTransferState = transferState;
}
if (doFire) {
fireTransferStateChanged();
}
if (doStopLocation) {
stopSourceLocation();
}
synchronized(lockObject) {
if (doInterrupt && transferTask != null) {
if (transferTask.cancel(true)) {
Logging.logCheckedDebug(LOG, "Cancelled/interrupted transfer task");
}
transferTask = null;
}
if (doNotify) {
lockObject.notifyAll();
}
}
}
/**
* Source location execution.
*/
private void locationExecution() {
String desiredID = masterID.toString();
Logging.logCheckedDebug(LOG, "Locating more data sources for ID: ", desiredID);
DiscoveryService discoveryService = peerGroup.getDiscoveryService();
if (allowLocalDiscovery) {
try {
Enumeration localAdvs =
discoveryService.getLocalAdvertisements(
DiscoveryService.ADV,
"ContentID", desiredID);
addDiscoveredSources(localAdvs);
} catch (IOException iox) {
Logging.logCheckedWarning(LOG, "Could not query local advertisements\n", iox);
}
}
if (allowRemoteDiscovery) {
discoveryService.getRemoteAdvertisements(
null, DiscoveryService.ADV,
"ContentID", desiredID,
discoveryThreshold, discoveryListener);
}
}
/**
* Adds the discovery results to our known list of allSources.
*/
private void addDiscoveredSources(Enumeration advs) {
while(advs.hasMoreElements()) {
Object adv = advs.nextElement();
try {
Logging.logCheckedDebug(LOG, "Discovered adv: ", adv);
if (adv instanceof ContentShareAdvertisement) {
addDiscoveredSource((ContentShareAdvertisement) adv);
}
} catch (ClassCastException castx) {
Logging.logCheckedDebug(LOG, "Found unusable advertisement: ", adv);
}
}
}
/**
* Checks to see if the source provided is known already. If not, it
* adds the advertisement to the list of known allSources and updates
* the location state as appropriate.
*/
private void addDiscoveredSource(ContentShareAdvertisement adv) {
boolean doFire;
boolean doStop;
synchronized(lockObject) {
if (allSources.contains(adv)) {
// Already found this source.
// LOGGING: was Finest
Logging.logCheckedDebug(LOG, "Advertisement was already found:\n", adv);
return;
}
if (uselessSources.contains(adv)) {
// Already found this one too.
// LOGGING: was Finest
Logging.logCheckedDebug(LOG, "Advertisement was already found to be unusable:\n", adv);
return;
}
Logging.logCheckedDebug(LOG, "Found advertisement: ", adv);
if (isAdvertisementOfUse(adv)) {
allSources.add(adv);
newSources.add(adv);
lockObject.notifyAll();
doFire = checkSources();
doStop = locationState.hasMany();
// LOGGING: was Finer
Logging.logCheckedDebug(LOG, "Sources known now at: ",
allSources.size(), " State: ", locationState);
} else {
// LOGGING: was Finer
Logging.logCheckedDebug(LOG, "Advertisement determined to not be usable");
uselessSources.add(adv);
doFire = false;
doStop = false;
}
}
if (doFire) {
fireLocationStateChanged();
}
if (doStop) {
stopSourceLocation();
}
}
/**
* Checks to see if we have "enough" and/or "many" allSources and updates
* the location state as appropriate.
*
* @return true if location state has changed, false otherwise
*/
private boolean checkSources() {
ContentSourceLocationState newState;
synchronized(lockObject) {
if (allSources.size() >= manySources) {
newState = ContentSourceLocationState.LOCATING_HAS_MANY;
} else if (allSources.size() >= enoughSources) {
newState = ContentSourceLocationState.LOCATING_HAS_ENOUGH;
} else {
newState = ContentSourceLocationState.LOCATING;
}
newState = newState.getEquivalent(locationState.isLocating());
if (locationState == newState) {
return false;
} else {
locationState = newState;
return true;
}
}
}
/**
* Notify listeners of a change in the source location state.
*/
private void fireLocationStateChanged() {
ContentTransferEvent event = null;
for (ContentTransferListener listener : listeners) {
if (event == null) {
event = createTransferEvent();
}
listener.contentLocationStateUpdated(event);
}
}
/**
* Notify listeners of a change in the source location state.
*/
private void fireTransferStateChanged() {
ContentTransferEvent event = null;
for (ContentTransferListener listener : listeners) {
if (event == null) {
event = createTransferEvent();
}
listener.contentTransferStateUpdated(event);
}
}
/**
* Creates a content transfer event containing our current status.
*/
private ContentTransferEvent createTransferEvent() {
synchronized(lockObject) {
return new ContentTransferEvent.Builder(this)
.locationCount(allSources.size())
.locationState(locationState)
.transferState(transferState)
.build();
}
}
/**
* Transfer execution mainline.
*/
@SuppressWarnings("fallthrough")
private void transferExecution() throws InterruptedException {
Logging.logCheckedDebug(LOG, "Transfer execution starting");
ContentTransferState attemptResult = ContentTransferState.PENDING;
List<ContentShareAdvertisement> newList =
new ArrayList<ContentShareAdvertisement>();
List<ContentShareAdvertisement> allList =
new ArrayList<ContentShareAdvertisement>();
int stalls = 0;
boolean doDelay;
boolean running = true;
try {
do {
// LOGGING: was Finer
Logging.logCheckedDebug(LOG, "Updating transfer attempt source lists");
doDelay = false;
allList.clear();
newList.clear();
synchronized(lockObject) {
if (!transferState.isRetrieving()) {
// We've been instructured to stop
Logging.logCheckedDebug(LOG, "Transfer state no longer retrieving");
break;
}
allList.addAll(allSources);
newList.addAll(newSources);
// Reset the new sources list
newSources.clear();
}
if (allList.size() + newList.size() == 0) {
Logging.logCheckedDebug(LOG, "No sources found. Waiting.");
stalls++;
synchronized(lockObject) {
transferState = ContentTransferState.STALLED;
doDelay = true;
}
} else {
boolean doCheck = false;
synchronized(lockObject) {
if (transferState != ContentTransferState.RETRIEVING) {
transferState = ContentTransferState.RETRIEVING;
doCheck = true;
}
}
if (doCheck) {
// Fire an event off immediately
checkTransferState();
}
try {
Logging.logCheckedDebug(LOG, "Transfer attempt commencing");
attemptResult = transferAttempt(
destFile, allList, newList);
Logging.logCheckedDebug(LOG, "Transfer attempt result: ", attemptResult);
switch(attemptResult) {
case CANCELLED:
cancel();
running = false;
break;
case FAILED:
throw(new TransferFailedException(
"Transfer attempt result: "
+ attemptResult));
case PENDING:
case RETRIEVING:
case STALLED:
// Remap these to something that makes sense
attemptResult = ContentTransferState.STALLED;
doDelay = true;
stalls++;
// Fall through...
case RETRYING:
synchronized(lockObject) {
transferState = attemptResult;
}
break;
case COMPLETED:
synchronized(lockObject) {
if (content == null) {
LOG.warn("Transfer attempt returned "
+ attemptResult
+ " but content was not set!");
} else {
transferState = attemptResult;
running = false;
}
}
break;
}
} catch (TransferException transx) {
Logging.logCheckedDebug(LOG, "Transfer attempt failed\n", transx);
// Irrecoverable failure. Bubble this up to user.
synchronized(lockObject) {
transferState = ContentTransferState.FAILED;
if (reason == null) {
reason = transx;
}
}
running = false;
}
}
if (stalls >= maxStalls) {
synchronized(lockObject) {
transferState = ContentTransferState.FAILED;
if (reason == null) {
reason = new TransferException(
"Maximum number of stalled transfer "
+ "attempts reached (" + maxStalls + ")");
}
}
}
checkTransferState();
if (doDelay) {
/*
* We wait with timeout to allow incoming advertisements
* to be acted on immediately after they arrive, since
* that is likely to be the cause of the stalls, etc..
*/
synchronized(lockObject) {
try {
lockObject.wait(sourceLocationInterval * 1000L);
} catch (InterruptedException intx) {
if (Thread.interrupted()) {
// Rethrow
throw(intx);
}
}
}
}
} while(running);
} catch (RuntimeException rtx) {
Logging.logCheckedDebug(LOG, "Caught runtime exception\n\n", rtx);
throw(rtx);
} finally {
Logging.logCheckedDebug(LOG, "Transfer execution exiting");
}
}
}