package org.limewire.ui.swing.search.model;
import static org.limewire.ui.swing.search.model.VisualSearchResult.NEW_SOURCES;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import javax.swing.SwingUtilities;
import org.limewire.collection.glazedlists.GlazedListsFactory;
import org.limewire.core.api.URN;
import org.limewire.core.api.download.DownloadAction;
import org.limewire.core.api.download.DownloadException;
import org.limewire.core.api.download.DownloadItem;
import org.limewire.core.api.download.DownloadListManager;
import org.limewire.core.api.search.GroupedSearchResult;
import org.limewire.core.api.search.Search;
import org.limewire.core.api.search.SearchCategory;
import org.limewire.core.api.search.SearchListener;
import org.limewire.core.api.search.SearchManager;
import org.limewire.core.api.search.SearchResultList;
import org.limewire.core.api.search.SearchDetails.SearchType;
import org.limewire.listener.EventListener;
import org.limewire.logging.Log;
import org.limewire.logging.LogFactory;
import org.limewire.ui.swing.components.DisposalListener;
import org.limewire.ui.swing.filter.FilterDebugger;
import org.limewire.ui.swing.search.SearchInfo;
import org.limewire.ui.swing.util.DownloadExceptionHandler;
import ca.odell.glazedlists.BasicEventList;
import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.FilterList;
import ca.odell.glazedlists.SortedList;
import ca.odell.glazedlists.TransactionList;
import ca.odell.glazedlists.matchers.AbstractMatcherEditor;
import ca.odell.glazedlists.matchers.Matcher;
import ca.odell.glazedlists.matchers.MatcherEditor;
import ca.odell.glazedlists.matchers.MatcherEditor.Event;
import ca.odell.glazedlists.util.concurrent.Lock;
import ca.odell.glazedlists.util.concurrent.ReadWriteLock;
import com.google.inject.Provider;
/**
* The default implementation of SearchResultsModel containing the results of
* a search. This assembles search results into grouped, filtered, and sorted
* lists, provides access to details about the search request, and handles
* requests to download a search result.
*/
class BasicSearchResultsModel implements SearchResultsModel, VisualSearchResultStatusListener {
private static final Log LOG = LogFactory.getLog(BasicSearchResultsModel.class);
private int downloadsStarted = 0;
/** Filter debugger associated with this model. */
private final FilterDebugger<VisualSearchResult> filterDebugger;
/** Descriptor containing search details. */
private final SearchInfo searchInfo;
/** Search request object. */
private final Search search;
/** Search manager. */
private final SearchManager searchManager;
/** Search result list. */
private final SearchResultList searchResultList;
/** Core download manager. */
private final DownloadListManager downloadListManager;
/** Download exception handler. */
private final Provider<DownloadExceptionHandler> downloadExceptionHandler;
/** Factory to create visual search results. */
private final VisualSearchResultFactory vsrFactory;
/** List of visual search results grouped and sorted by URN. */
private final EventList<VisualSearchResult> groupedUrnResults;
/** A TransactionList for upstream changes. */
private final TransactionList<VisualSearchResult> transactionList;
/** Filtered list of grouped search results. */
private final FilterList<VisualSearchResult> filteredResultList;
/** Listener to handle search request events. */
private SearchListener searchListener;
/** Listener to handle search result list events. */
private EventListener<Collection<GroupedSearchResult>> searchListListener;
/** Current list of sorted and filtered results. */
private SortedList<VisualSearchResult> sortedResultList;
/** Current list of visible, sorted and filtered results. */
private FilterList<VisualSearchResult> visibleResultList;
/** Current selected search category. */
private SearchCategory selectedCategory;
/** Current sort option. */
private SortOption sortOption;
/** Current matcher editor for filtered search results. */
private MatcherEditor<VisualSearchResult> filterEditor;
/** Listener for filter editor. */
private MatcherEditor.Listener<VisualSearchResult> filterEditorListener;
/** Matcher editor for visible search results. */
private final VisibleMatcherEditor visibleEditor = new VisibleMatcherEditor();
private List<DisposalListener> disposalListeners = new ArrayList<DisposalListener>();
/** A list of listeners for changes. */
private final List<VisualSearchResultStatusListener> changeListeners;
/** A queue used to collect search results and apply them in bulk. */
private final ListQueuer listQueuer = new ListQueuer();
/** Comparator that searches through the list of results & finds them based on URN. */
private final UrnResultFinder resultFinder = new UrnResultFinder();
//TODO Using this to fix a case where events are coming in for items no longer in the list after a clear.
//We should remove this after the release and fix the root cause, that DownloadListeners for visual search results
//are not being removed from the downloaders after search tabs are removed or refreshed.
private boolean cleared = false;
/**
* Constructs a BasicSearchResultsModel with the specified search details,
* search request object, and services.
*/
public BasicSearchResultsModel(SearchInfo searchInfo, Search search,
VisualSearchResultFactory vsrFactory,
DownloadListManager downloadListManager,
Provider<DownloadExceptionHandler> downloadExceptionHandler,
SearchManager searchManager) {
this.searchInfo = searchInfo;
this.search = search;
this.vsrFactory = vsrFactory;
this.searchManager = searchManager;
this.downloadListManager = downloadListManager;
this.downloadExceptionHandler = downloadExceptionHandler;
changeListeners = new ArrayList<VisualSearchResultStatusListener>(3);
// Create filter debugger.
filterDebugger = new FilterDebugger<VisualSearchResult>();
// Create core list of grouped results.
searchResultList = searchManager.addSearch(search, searchInfo);
// Create local list of grouped visual results. This list is always
// updated on the EDT, so we optimize performance by using a dummy lock.
groupedUrnResults = new BasicEventList<VisualSearchResult>(new ReadWriteLock() {
private final Lock noopLock = new Lock() {
@Override public void lock() {}
@Override public boolean tryLock() { return true; }
@Override public void unlock() {}
};
@Override public Lock readLock() {
return noopLock;
}
@Override public Lock writeLock() {
return noopLock;
}
});
// Create transaction list.
transactionList = GlazedListsFactory.transactionList(groupedUrnResults);
// Create filtered list.
filteredResultList = GlazedListsFactory.filterList(transactionList);
// Initialize display category and sorted list.
setSelectedCategory(searchInfo.getSearchCategory());
}
/**
* Installs the specified search listener and starts the search. The
* search listener should handle search results by calling the
* <code>addSearchResult(SearchResult)</code> method.
*/
@Override
public void start(SearchListener searchListener) {
if (searchListener == null) {
throw new IllegalArgumentException("Search listener cannot be null");
}
// Install search listener.
this.searchListener = searchListener;
search.addSearchListener(searchListener);
// Install listener to handle new grouped results. The event is
// usually fired by a background thread.
searchListListener = new EventListener<Collection<GroupedSearchResult>>() {
@Override
public void handleEvent(Collection<GroupedSearchResult> results) {
// Add results to queue for processing. The queue is used to
// improve performance for Browse Host requests, which can
// generate thousands of results one at a time.
listQueuer.addAll(results);
}
};
searchResultList.addListener(searchListListener);
// Start search.
search.start();
}
/**
* Adds the specified list of grouped results to the list of visual
* results.
*/
void addResultsInternal(final List<GroupedSearchResult> resultList) {
// Process next group of results. We use a transaction list so that
// large result sets generate a single list change event. This
// improves the performance for Browse Host/Friend requests, which can
// generate thousands of results.
transactionList.beginEvent(true);
try {
for (GroupedSearchResult gsr : resultList) {
URN urn = gsr.getUrn();
int idx = Collections.binarySearch(groupedUrnResults, urn, resultFinder);
if (idx < 0) {
// URN not found so add visual result at insertion point.
// This keeps the list in sorted order.
idx = -(idx + 1);
VisualSearchResult vsr = vsrFactory.create(gsr, this);
groupedUrnResults.add(idx, vsr);
// Notify listeners to update result states.
for (VisualSearchResultStatusListener listener : changeListeners) {
listener.resultCreated(vsr);
}
} else {
// URN found so replace result to generate change event.
VisualSearchResult vsr = groupedUrnResults.get(idx);
groupedUrnResults.set(idx, vsr);
// Notify listeners to update similar results.
for (VisualSearchResultStatusListener listener : changeListeners) {
listener.resultChanged(vsr, NEW_SOURCES, null, null);
}
}
}
} finally {
transactionList.commitEvent();
}
// Reapply sort in case similarity parents have changed.
if (sortedResultList != null) {
sortedResultList.setComparator(sortedResultList.getComparator());
}
}
/**
* Stops the search and removes the current search listener.
*/
@Override
public void dispose() {
// Stop search.
search.stop();
// Remove search listeners.
if (searchListener != null) {
search.removeSearchListener(searchListener);
searchListener = null;
}
if (searchListListener != null) {
searchResultList.removeListener(searchListListener);
searchListListener = null;
}
// Remove search from core management.
searchManager.removeSearch(search);
groupedUrnResults.dispose();
notifyDisposalListeners();
}
@Override
public SearchCategory getFilterCategory() {
return searchInfo.getSearchCategory();
}
@Override
public FilterDebugger<VisualSearchResult> getFilterDebugger() {
return filterDebugger;
}
@Override
public EventList<VisualSearchResult> getUnfilteredList() {
return transactionList;
}
@Override
public EventList<VisualSearchResult> getFilteredList() {
return filteredResultList;
}
@Override
public SearchCategory getSearchCategory() {
return searchInfo.getSearchCategory();
}
@Override
public String getSearchQuery() {
return searchInfo.getSearchQuery();
}
@Override
public String getSearchTitle() {
return searchInfo.getTitle();
}
@Override
public int getResultCount() {
return searchResultList.getResultCount();
}
@Override
public SearchType getSearchType() {
return searchInfo.getSearchType();
}
/**
* Returns a list of filtered results.
*/
@Override
public EventList<VisualSearchResult> getFilteredSearchResults() {
return filteredResultList;
}
/**
* Returns a list of sorted and filtered results for the selected search
* category and sort option. Only visible results are included in the list.
*/
@Override
public EventList<VisualSearchResult> getSortedSearchResults() {
return visibleResultList;
}
/**
* Returns the selected search category.
*/
@Override
public SearchCategory getSelectedCategory() {
return selectedCategory;
}
/**
* Selects the specified search category. If the selected category is
* changed, this method updates the sorted list.
*/
@Override
public void setSelectedCategory(SearchCategory selectedCategory) {
if (this.selectedCategory != selectedCategory) {
this.selectedCategory = selectedCategory;
updateSortedList();
}
}
/**
* Updates sorted list of visible results. This method is called when the
* selected category is changed.
*/
private void updateSortedList() {
// Create visible and sorted lists if necessary.
if (visibleResultList == null) {
sortedResultList = GlazedListsFactory.sortedList(filteredResultList, sortOption != null ? SortFactory.getSortComparator(sortOption) : null);
visibleResultList = GlazedListsFactory.filterList(sortedResultList, visibleEditor);
}
}
/**
* Sets the sort option. This method updates the sorted list by changing
* the sort comparator.
*/
@Override
public void setSortOption(SortOption sortOption) {
this.sortOption = sortOption;
sortedResultList.setComparator((sortOption != null) ? SortFactory.getSortComparator(sortOption) : null);
}
@Override
public void setFilterEditor(MatcherEditor<VisualSearchResult> editor) {
// Remove listener from existing filter editor.
if ((filterEditor != null) && (filterEditorListener != null)) {
filterEditor.removeMatcherEditorListener(filterEditorListener);
}
// Create listener to handle changes to the filter.
if (filterEditorListener == null) {
filterEditorListener = new MatcherEditor.Listener<VisualSearchResult>() {
@Override
public void changedMatcher(Event<VisualSearchResult> matcherEvent) {
// Post Runnable on event queue to update filtered list for
// visible items. This allows us to display child results
// whose parents become hidden due to filtering.
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
visibleEditor.update();
}
});
}
};
}
// Set filter editor and add listener.
filterEditor = editor;
filterEditor.addMatcherEditorListener(filterEditorListener);
// Apply filter editor.
filteredResultList.setMatcherEditor(filterEditor);
}
public void addResultListener(VisualSearchResultStatusListener vsrChangeListener) {
changeListeners.add(vsrChangeListener);
}
@Override
public void resultCreated(VisualSearchResult vsr) {
}
@Override
public void resultsCleared() {
}
@Override
public void resultChanged(VisualSearchResult vsr, String propertyName, Object oldValue, Object newValue) {
// Scan through the list & find the item.
URN urn = vsr.getUrn();
int idx = Collections.binarySearch(groupedUrnResults, urn, resultFinder);
//TODO clean up, see comment about cleared variable, and why it should be removed.
assert cleared || idx >= 0;
if (idx >=0) {
VisualSearchResult existing = groupedUrnResults.get(idx);
VisualSearchResult replaced = groupedUrnResults.set(idx, existing);
assert cleared || replaced == vsr;
if(replaced == vsr) {
for(VisualSearchResultStatusListener listener : changeListeners) {
listener.resultChanged(vsr, propertyName, oldValue, newValue);
}
}
}
}
/**
* Removes all results from the model
*/
@Override
public void clear() {
listQueuer.clear();
}
/**
* Initiates a download of the specified visual search result.
*/
@Override
public void download(VisualSearchResult vsr) {
download(vsr, null);
}
/**
* Initiates a download of the specified visual search result to the
* specified save file.
*/
@Override
public void download(final VisualSearchResult vsr, File saveFile) {
try {
downloadsStarted++;
// Add download to manager. If save file is specified, then set
// overwrite to true because the user has already confirmed it.
DownloadItem di = (saveFile == null) ?
downloadListManager.addDownload(search, vsr.getCoreSearchResults()) :
downloadListManager.addDownload(search, vsr.getCoreSearchResults(), saveFile, true);
// Add listener, and initialize download state.
di.addPropertyChangeListener(new DownloadItemPropertyListener(vsr));
BasicDownloadState state = BasicDownloadState.fromState(di.getState());
if(state != null) {
vsr.setDownloadState(state);
}
} catch (final DownloadException e) {
if (e.getErrorCode() == DownloadException.ErrorCode.FILE_ALREADY_DOWNLOADING) {
DownloadItem downloadItem = downloadListManager.getDownloadItem(vsr.getUrn());
if (downloadItem != null) {
downloadItem.addPropertyChangeListener(new DownloadItemPropertyListener(vsr));
BasicDownloadState state = BasicDownloadState.fromState(downloadItem.getState());
if(state != null) {
vsr.setDownloadState(state);
}
if (saveFile != null) {
try {
// Update save file in DownloadItem.
downloadItem.setSaveFile(saveFile, true);
} catch (DownloadException ex) {
LOG.infof(ex, "Unable to relocate downloading file {0}", ex.getMessage());
}
}
}
} else {
downloadExceptionHandler.get().handleDownloadException(new DownloadAction() {
@Override
public void download(File saveFile, boolean overwrite)
throws DownloadException {
DownloadItem di = downloadListManager.addDownload(search, vsr.getCoreSearchResults(), saveFile, overwrite);
di.addPropertyChangeListener(new DownloadItemPropertyListener(vsr));
BasicDownloadState state = BasicDownloadState.fromState(di.getState());
if(state != null) {
vsr.setDownloadState(state);
}
}
@Override
public void downloadCanceled(DownloadException ignored) {
//nothing to do
}
}, e, true);
}
}
}
/**
* Returns true if the specified search result is allowed by the current
* filter editor. This means the result is a member of the filtered list.
*/
private boolean isFilterMatch(VisualSearchResult vsr) {
if (filterEditor != null) {
return filterEditor.getMatcher().matches(vsr);
}
return true;
}
/**
* A matcher editor used to filter visible search results.
*/
private class VisibleMatcherEditor extends AbstractMatcherEditor<VisualSearchResult> {
public VisibleMatcherEditor() {
currentMatcher = new Matcher<VisualSearchResult>() {
@Override
public boolean matches(VisualSearchResult item) {
// Determine whether item has parent, and parent is hidden
// due to filtering. If so, we treat the item as visible.
VisualSearchResult parent = item.getSimilarityParent();
boolean parentHidden = (parent != null) && !isFilterMatch(parent);
return item.isVisible() || parentHidden;
}
};
}
/**
* Updates the list by firing a matcher change event to registered
* listeners.
*/
public void update() {
fireChangedMatcher(new MatcherEditor.Event<VisualSearchResult>(
this, Event.CHANGED, currentMatcher));
}
}
@Override
public void addDisposalListener(DisposalListener listener) {
disposalListeners.add(listener);
}
@Override
public void removeDisposalListener(DisposalListener listener) {
disposalListeners.remove(listener);
}
private void notifyDisposalListeners(){
for (DisposalListener listener : disposalListeners){
listener.objectDisposed(this);
}
}
/**
* A queue used to collect search results and apply them in bulk.
* ListQueuer is used to protect the performance of UI event processing
* when thousands of individual results arrive in rapid succession. When
* a result is received, it is added to the queue and scheduled for later
* processing on the event queue. If another result is received before
* the event is handled, then it is added to the queue so it can be
* processed together with other pending results.
*/
private class ListQueuer implements Runnable {
private final Object LOCK = new Object();
/** Queue of pending results; should always be accessed under LOCK. */
private final List<GroupedSearchResult> queue = new ArrayList<GroupedSearchResult>();
private final List<GroupedSearchResult> transferQ = new ArrayList<GroupedSearchResult>();
private boolean scheduled = false;
/**
* Adds the specified collection of results to the queue, and schedules
* an event to add them to the result list.
*/
void addAll(Collection<? extends GroupedSearchResult> results) {
synchronized(LOCK) {
queue.addAll(results);
schedule();
}
}
/**
* Clears the queue and posts an event to clear all search results.
*/
void clear() {
synchronized(LOCK) {
queue.clear();
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
cleared = true;
// Clear result list.
searchResultList.clear();
groupedUrnResults.clear();
// Notify change listeners that results are cleared.
for (VisualSearchResultStatusListener listener : changeListeners) {
listener.resultsCleared();
}
}
});
}
}
/**
* Posts event to process queued results. This method should only be
* called while holding the lock.
*/
private void schedule() {
if (!scheduled) {
scheduled = true;
// Purposely use SwingUtilities, and not SwingUtils, so that
// that we force an event on the end of the event queue.
SwingUtilities.invokeLater(this);
}
}
@Override
public void run() {
// Move result into transferQ inside the lock.
transferQ.clear();
synchronized(LOCK) {
transferQ.addAll(queue);
queue.clear();
}
// Add to search results outside the lock so we don't hold the
// lock while events are being triggered.
addResultsInternal(transferQ);
transferQ.clear();
synchronized(LOCK) {
scheduled = false;
// If the queue isn't empty, we need to reschedule ourselves
// because results got added to the queue without scheduling
// itself since we had scheduled=true.
if (!queue.isEmpty()) {
schedule();
}
}
}
}
/**
* Comparator to search for VisualSearchResult objects by URN. This is
* only used to perform a binary search by URN, never to perform the sort,
* so the compare() method does not need to be symmetric.
*/
private static class UrnResultFinder implements Comparator<Object> {
@Override
public int compare(Object o1, Object o2) {
return ((VisualSearchResult)o1).getUrn().compareTo(((URN)o2));
}
}
}