package org.limewire.core.impl.search.browse;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.limewire.core.api.search.Search;
import org.limewire.core.api.search.SearchListener;
import org.limewire.core.api.search.SearchResult;
import org.limewire.core.api.search.browse.BrowseSearch;
import org.limewire.core.api.search.browse.BrowseSearchFactory;
import org.limewire.core.api.search.browse.BrowseStatus;
import org.limewire.core.api.search.browse.BrowseStatusListener;
import org.limewire.core.api.search.browse.BrowseStatus.BrowseState;
import org.limewire.friend.api.Friend;
import org.limewire.friend.api.FriendPresence;
import org.limewire.logging.Log;
import org.limewire.logging.LogFactory;
/**
* Aggregates multiple AnonymousSingleBrowseSearches and FriendSingleBrowseSearches.
*
*/
class MultipleBrowseSearch extends AbstractBrowseSearch {
private static final Log LOG = LogFactory.getLog(MultipleBrowseSearch.class);
private static final int PARALLEL_BROWSES = 2;
private final CombinedSearchListener combinedSearchListener = new CombinedSearchListener();
private final CombinedBrowseStatusListener combinedBrowseStatusListener = new CombinedBrowseStatusListener();
private final List<BrowseSearch> activeBrowses = new CopyOnWriteArrayList<BrowseSearch>();
private final List<FriendPresence> presences;
private final Queue<FriendPresence> pendingPresences;
private final BrowseSearchFactory browseSearchFactory;
/**
* @param presences the people to be browsed. Can not be null.
*/
public MultipleBrowseSearch(BrowseSearchFactory browseSearchFactory, Collection<FriendPresence> presences) {
this.browseSearchFactory = browseSearchFactory;
this.presences = new ArrayList<FriendPresence>(presences);
this.pendingPresences = new ConcurrentLinkedQueue<FriendPresence>();
}
@Override
public void start() {
pendingPresences.addAll(presences);
// Start PARALLEL_BROWSES browses -- as each one finishes, it will start the next browse.
// This prevents us from doing more than PARALLEL_BROWSES browses in parallel.
for(int i = 0; i < PARALLEL_BROWSES && !pendingPresences.isEmpty(); i++) {
if(!startPendingBrowse()) {
break;
}
}
}
/** Starts a pending browse. Returns true if it succesfully started. */
private boolean startPendingBrowse() {
FriendPresence host = pendingPresences.poll();
if(host != null) {
LOG.debugf("Starting browse for host {0}", host);
BrowseSearch browse = browseSearchFactory.createBrowseSearch(host);
browse.addSearchListener(combinedSearchListener);
browse.addBrowseStatusListener(combinedBrowseStatusListener);
activeBrowses.add(browse);
browse.start();
return true;
} else {
LOG.debugf("Attempted to start pending browse, but no hosts left.");
return false;
}
}
@Override
public void stop() {
// order here is very important --
// we clear pending hosts first, so that
// stopped browses don't start another pending browse.
pendingPresences.clear();
for (BrowseSearch browse: activeBrowses){
browse.stop();
}
}
@Override
public void repeat() {
// order here is very important --
// we stop first and then clear,
// so that events from the stop
// get cleared out.
stop();
combinedBrowseStatusListener.clear();
combinedSearchListener.clear();
start();
}
private class CombinedSearchListener implements SearchListener {
/** Keeps count of how many browses have completed */
private AtomicInteger stoppedBrowses = new AtomicInteger(0);
@Override
public void handleSearchResult(Search search, SearchResult searchResult) {
for (SearchListener listener : searchListeners) {
listener.handleSearchResult(MultipleBrowseSearch.this, searchResult);
}
}
@Override
public void handleSearchResults(Search search, Collection<? extends SearchResult> searchResults) {
for (SearchListener listener : searchListeners) {
listener.handleSearchResults(MultipleBrowseSearch.this, searchResults);
}
}
/** Clears the count of completed browses */
public void clear() {
stoppedBrowses.set(0);
}
@Override
public void searchStarted(Search search) {
for (SearchListener listener : searchListeners) {
listener.searchStarted(MultipleBrowseSearch.this);
}
}
@Override
public void searchStopped(Search search) {
LOG.debugf("Received search stopped event {0}", search);
if (stoppedBrowses.incrementAndGet() == presences.size()) {
//all of our browses have completed
for (SearchListener listener : searchListeners) {
listener.searchStopped(MultipleBrowseSearch.this);
}
}
}
}
private class CombinedBrowseStatusListener implements BrowseStatusListener {
/** List of all failed browses (Friends includes anonymous) */
private List<Friend> failedList = new CopyOnWriteArrayList<Friend>();
/** The number of BrowseSearches in the LOADED state */
private AtomicInteger loaded = new AtomicInteger(0);
/** Whether or not there are updates in any of the browses */
private AtomicBoolean hasUpdated = new AtomicBoolean(false);
@Override
public void statusChanged(BrowseStatus status) {
LOG.debugf("Received status change event {0}", status);
switch(status.getState()) {
case FAILED:
case OFFLINE:
//getFailedFriends() will only return 1 person
//since status is from a single browse
failedList.addAll(status.getFailedFriends());
break;
case UPDATED:
hasUpdated.set(true);
break;
case LOADED:
loaded.incrementAndGet();
break;
}
BrowseState state = getReleventMultipleBrowseState(status);
if (state != null) {
BrowseStatus browseStatus = new BrowseStatus(MultipleBrowseSearch.this, state, failedList.toArray(new Friend[failedList.size()]));
for (BrowseStatusListener listener : browseStatusListeners) {
listener.statusChanged(browseStatus);
}
}
activeBrowses.remove(status.getBrowseSearch());
startPendingBrowse();
}
/**
* Clears all cached data about the browses
*/
public void clear(){
hasUpdated.set(false);
loaded.set(0);
failedList.clear();
}
/**
* @return The aggregated status of the browses. For example if the browses are a mix of
* FAILED and LOADED, the status will be PARTIAL_FAIL. If the browses contain, FAILED,
* LOADED, and UPDATED, it will be UPDATED_PARTIAL_FAIL.
*/
private BrowseState getReleventMultipleBrowseState(BrowseStatus status){
if(loaded.get() == presences.size()){
return BrowseState.LOADED;
} else if(failedList.size() == presences.size()){
return BrowseState.FAILED;
} else if (failedList.size() > 0) {
if (loaded.get() > 0) {
if (hasUpdated.get()) {
return BrowseState.UPDATED_PARTIAL_FAIL;
} else {
return BrowseState.PARTIAL_FAIL;
}
}
} else if (hasUpdated.get()){
return BrowseState.UPDATED;
}
return BrowseState.LOADING;
}
}
}