package org.limewire.core.impl.upload;
import java.awt.EventQueue;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.limewire.collection.glazedlists.GlazedListsFactory;
import org.limewire.core.api.upload.UploadItem;
import org.limewire.core.api.upload.UploadListManager;
import org.limewire.core.api.upload.UploadState;
import org.limewire.core.impl.friend.BittorrentPresence;
import org.limewire.core.impl.friend.GnutellaPresence;
import org.limewire.core.settings.SharingSettings;
import org.limewire.friend.api.FriendManager;
import org.limewire.friend.api.FriendPresence;
import org.limewire.inject.EagerSingleton;
import org.limewire.io.ConnectableImpl;
import org.limewire.lifecycle.ServiceScheduler;
import org.limewire.listener.SwingSafePropertyChangeSupport;
import ca.odell.glazedlists.BasicEventList;
import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.GlazedLists;
import ca.odell.glazedlists.ObservableElementList;
import ca.odell.glazedlists.impl.ThreadSafeList;
import com.google.inject.Inject;
import com.google.inject.name.Named;
import com.limegroup.bittorrent.BTUploader;
import com.limegroup.gnutella.UploadServices;
import com.limegroup.gnutella.Uploader;
import com.limegroup.gnutella.Uploader.UploadStatus;
@EagerSingleton
public class CoreUploadListManager implements UploadListener, UploadListManager {
private final CoreUploadItem.Factory cuiFactory;
private final UploadServices uploadServices;
private final FriendManager friendManager;
private final PropertyChangeSupport changeSupport = new SwingSafePropertyChangeSupport(this);
private final EventList<UploadItem> uploadItems;
private EventList<UploadItem> swingThreadUploadItems;
private ThreadSafeList<UploadItem> threadSafeUploadItems;
private static final int PERIOD = 1000;
@Inject
public CoreUploadListManager(UploadServices uploadServices, FriendManager friendManager, CoreUploadItem.Factory cuiFactory) {
this.cuiFactory = cuiFactory;
this.uploadServices = uploadServices;
this.friendManager = friendManager;
threadSafeUploadItems = GlazedListsFactory.threadSafeList(new BasicEventList<UploadItem>());
ObservableElementList.Connector<UploadItem> uploadConnector = GlazedLists.beanConnector(UploadItem.class);
uploadItems = GlazedListsFactory.observableElementList(threadSafeUploadItems, uploadConnector) ;
}
@Inject
public void register(UploadListenerList uploadListenerList) {
uploadListenerList.addUploadListener(this);
}
/**
* Prepare and install the (polling) monitor service. This service will only be started when
* the stage keyed by *this* object is initiated.
*/
// TODO: Come up with a reasonable strategy for ServiceRegistry custom stage keys (Strings vs Hashes?)
@Inject
public void register(ServiceScheduler scheduler, @Named("backgroundExecutor") ScheduledExecutorService executor) {
Runnable command = new Runnable() {
@Override
public void run() {
update();
}
};
scheduler.scheduleWithFixedDelay("UI Upload Status Monitor",
command, 0, PERIOD,
TimeUnit.MILLISECONDS, executor).in(this);
}
@Override
public List<UploadItem> getUploadItems() {
return uploadItems;
}
@Override
public EventList<UploadItem> getSwingThreadSafeUploads() {
assert EventQueue.isDispatchThread();
if (swingThreadUploadItems == null) {
swingThreadUploadItems = GlazedListsFactory.swingThreadProxyEventList(uploadItems);
}
return swingThreadUploadItems;
}
/**
* Adds the specified listener to the list that is notified when a
* property value changes. Listeners added from the Swing UI thread will
* always receive notification events on the Swing UI thread.
*/
@Override
public void addPropertyChangeListener(PropertyChangeListener listener) {
changeSupport.addPropertyChangeListener(listener);
}
/**
* Removes the specified listener from the list that is notified when a
* property value changes.
*/
@Override
public void removePropertyChangeListener(PropertyChangeListener listener) {
changeSupport.removePropertyChangeListener(listener);
}
/**
* Checks for uploads in progress, and fires a property change event if
* all uploads are completed.
*/
@Override
public void updateUploadsCompleted() {
if (uploadServices.getNumUploads() == 0) {
uploadsCompleted();
}
}
@Override
public void uploadAdded(Uploader uploader) {
if (!uploader.getUploadType().isInternal()) {
CoreUploadItem currentItem = findMatchingUploader(uploader);
CoreUploadItem newItem = cuiFactory.create(uploader, getFriendPresence(uploader));
// if there's no matching uploader, add as a new upload
if(currentItem == null) {
threadSafeUploadItems.add(newItem);
} else {
threadSafeUploadItems.getReadWriteLock().writeLock().lock();
try {
threadSafeUploadItems.set(threadSafeUploadItems.indexOf(currentItem), newItem);
} finally {
threadSafeUploadItems.getReadWriteLock().writeLock().unlock();
}
}
}
}
/**
* Attempts to match a newly added uploader with one that already exists in the list.
* If an uploader already exists that matches this Uploader, it returns the pre-existing
* CoreUploadItem, otherwise returns null.
*/
private CoreUploadItem findMatchingUploader(Uploader uploader) {
CoreUploadItem matchingItem = null;
threadSafeUploadItems.getReadWriteLock().readLock().lock();
try {
for(UploadItem item : threadSafeUploadItems) {
CoreUploadItem coreUploadItem = (CoreUploadItem) item;
if(coreUploadItem.getUploader().getHost().equals(uploader.getHost())) {
// if its a browse host, there's no file to match on
if(uploader.getState() == UploadStatus.BROWSE_HOST && uploader.getUploadType() == coreUploadItem.getUploader().getUploadType()) {
matchingItem = coreUploadItem;
break;
} else if(uploader.getFile() != null && uploader.getFile().equals(coreUploadItem.getFile())) {
matchingItem = coreUploadItem;
break;
}
}
}
} finally {
threadSafeUploadItems.getReadWriteLock().readLock().unlock();
}
return matchingItem;
}
@Override
public void uploadComplete(Uploader uploader) {
CoreUploadItem item = cuiFactory.create(uploader, getFriendPresence(uploader));
//alert item that it really is finished so that getState() will be correct
item.finish();
UploadState state = item.getState();
if (state == UploadState.CANCELED) {
//cancelled items should be removed immediately
remove(item);
} else if (state == UploadState.DONE || state == UploadState.BROWSE_HOST_DONE || state.isError()) {
if (SharingSettings.CLEAR_UPLOAD.getValue()) {
//Remove if auto-clear is enabled.
remove(item);
} else {
//make sure upload state is correct and UI is informed of state change
int i = threadSafeUploadItems.indexOf(item);
if (i>-1) {
((CoreUploadItem)threadSafeUploadItems.get(i)).finish();
}
}
}
}
@Override
public void uploadsCompleted() {
changeSupport.firePropertyChange(UPLOADS_COMPLETED, false, true);
}
// forces refresh
void update() {
uploadItems.getReadWriteLock().writeLock().lock();
try {
// TODO use TransactionList for these for performance (requires using GlazedLists from head)
for (UploadItem item : uploadItems) {
if (item.getState() != UploadState.DONE && item.getState() != UploadState.BROWSE_HOST_DONE && item instanceof CoreUploadItem)
((CoreUploadItem) item).refresh();
}
} finally {
uploadItems.getReadWriteLock().writeLock().unlock();
}
}
private FriendPresence getFriendPresence(Uploader uploader) {
if(uploader instanceof BTUploader) {
return new BittorrentPresence(uploader);
}
String id = uploader.getPresenceId();
FriendPresence currentPresence = null;
if (id != null) {
currentPresence = friendManager.getMostRelevantFriendPresence(id);
}
if (currentPresence == null) {
// copy construct connectable to give it full equals semantics
currentPresence = new GnutellaPresence.GnutellaPresenceWithString(new ConnectableImpl(uploader), uploader.getHost());
}
return currentPresence;
}
/**
* Thread safe method which removes any finished uploads from management.
*/
@Override
public void clearFinished() {
List<UploadItem> finishedItems = new ArrayList<UploadItem>();
threadSafeUploadItems.getReadWriteLock().writeLock().lock();
try {
for(UploadItem item : threadSafeUploadItems) {
UploadState state = item.getState();
if (state.isFinished() || state.isError()) {
finishedItems.add(item);
}
}
threadSafeUploadItems.removeAll(finishedItems);
} finally {
threadSafeUploadItems.getReadWriteLock().writeLock().unlock();
}
}
/**
* Thread safe method which force removes an upload item from management.
*/
@Override
public void remove(UploadItem item) {
threadSafeUploadItems.remove(item);
}
}