package org.limewire.core.impl.search;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
import org.limewire.core.api.FilePropertyKey;
import org.limewire.core.api.search.Search;
import org.limewire.core.api.search.SearchCategory;
import org.limewire.core.api.search.SearchDetails;
import org.limewire.core.api.search.SearchEvent;
import org.limewire.core.api.search.SearchListener;
import org.limewire.core.api.search.SearchResult;
import org.limewire.core.impl.library.FriendSearcher;
import org.limewire.core.impl.search.torrentweb.TorrentWebSearchFactory;
import org.limewire.core.settings.SearchSettings;
import org.limewire.io.GUID;
import org.limewire.io.IpPort;
import org.limewire.listener.EventBroadcaster;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.name.Named;
import com.limegroup.gnutella.RemoteFileDesc;
import com.limegroup.gnutella.SearchServices;
import com.limegroup.gnutella.messages.QueryReply;
import com.limegroup.gnutella.xml.LimeXMLDocumentFactory;
public class CoreSearch implements Search, SearchListener {
private final SearchDetails searchDetails;
private final SearchServices searchServices;
private final QueryReplyListenerList listenerList;
private final FriendSearcher friendSearcher;
private final RemoteFileDescAdapter.Factory remoteFileDescAdapterFactory;
/**
* A search is considered processed when it is acted upon (started or stopped)
* <pre>
* -cannot repeat a search that has not yet been processed
* -cannot start a search that has already been processed
* -stopping a search only stops searches that have already been processed.
* </pre>
*/
final AtomicBoolean processingStarted = new AtomicBoolean(false);
private final CopyOnWriteArrayList<SearchListener> searchListeners = new CopyOnWriteArrayList<SearchListener>();
private final QrListener qrListener = new QrListener();
private final FriendSearchListener friendSearchListener = new FriendSearchListenerImpl();
private final ScheduledExecutorService backgroundExecutor;
private final EventBroadcaster<SearchEvent> searchEventBroadcaster;
private final AdvancedQueryStringBuilder compositeQueryBuilder;
/**
* The guid of the last active search.
*/
volatile byte[] searchGuid;
private final TorrentWebSearchFactory torrentWebSearchFactory;
private volatile Search torrentWebSearch;
@Inject
public CoreSearch(@Assisted SearchDetails searchDetails,
SearchServices searchServices,
QueryReplyListenerList listenerList,
FriendSearcher friendSearcher,
@Named("backgroundExecutor") ScheduledExecutorService backgroundExecutor,
EventBroadcaster<SearchEvent> searchEventBroadcaster,
LimeXMLDocumentFactory xmlDocumentFactory,
AdvancedQueryStringBuilder compositeQueryBuilder,
RemoteFileDescAdapter.Factory remoteFileDescAdapterFactory,
TorrentWebSearchFactory googleTorrentSearchFactory) {
this.searchDetails = searchDetails;
this.searchServices = searchServices;
this.listenerList = listenerList;
this.friendSearcher = friendSearcher;
this.backgroundExecutor = backgroundExecutor;
this.searchEventBroadcaster = searchEventBroadcaster;
this.compositeQueryBuilder = compositeQueryBuilder;
this.remoteFileDescAdapterFactory = remoteFileDescAdapterFactory;
this.torrentWebSearchFactory = googleTorrentSearchFactory;
}
@Override
public SearchCategory getCategory() {
return searchDetails.getSearchCategory();
}
@Override
public void addSearchListener(SearchListener searchListener) {
searchListeners.add(searchListener);
}
@Override
public void removeSearchListener(SearchListener searchListener) {
searchListeners.remove(searchListener);
}
@Override
public void start() {
if (processingStarted.getAndSet(true)) {
throw new IllegalStateException("cannot start search which has already been processed!");
}
for(SearchListener listener : searchListeners) {
listener.searchStarted(this);
}
doSearch(true);
}
private void doSearch(boolean initial) {
searchEventBroadcaster.broadcast(new SearchEvent(this, SearchEvent.Type.STARTED));
searchGuid = searchServices.newQueryGUID();
listenerList.addQueryReplyListener(searchGuid, qrListener);
switch(searchDetails.getSearchType()) {
case KEYWORD:
doKeywordSearch(initial);
break;
case WHATS_NEW:
doWhatsNewSearch(initial);
break;
}
}
private void doWhatsNewSearch(boolean initial) {
searchServices.queryWhatIsNew(searchGuid,
searchDetails.getSearchCategory());
// TODO: Search friends too.
}
private void doKeywordSearch(boolean initial) {
String query = searchDetails.getSearchQuery();
String advancedQuery = "";
Map<FilePropertyKey, String> advancedSearch = searchDetails.getAdvancedDetails();
if(advancedSearch != null && advancedSearch.size() > 0) {
if(query == null || query.equals("")) {
query = compositeQueryBuilder.createSimpleCompositeQuery(advancedSearch);
}
advancedQuery = compositeQueryBuilder.createXMLQueryString(advancedSearch, searchDetails.getSearchCategory().getCategory());
}
String mutated = searchServices.mutateQuery(query);
searchServices.query(searchGuid, mutated, advancedQuery,
searchDetails.getSearchCategory());
backgroundExecutor.execute(new Runnable() {
@Override
public void run() {
friendSearcher.doSearch(searchDetails, friendSearchListener);
}
});
if (initial
&& SearchSettings.USE_TORRENT_WEB_SEARCH.getValue()
&& searchDetails.getSearchCategory() == SearchCategory.TORRENT) {
torrentWebSearch = torrentWebSearchFactory.create(searchDetails.getSearchQuery());
torrentWebSearch.addSearchListener(this);
torrentWebSearch.start();
}
}
/**
* Stops current search and repeats search.
*
* @throws IllegalStateException If search processing has already begun (started or stopped)
*/
@Override
public void repeat() {
if(!processingStarted.get()) {
throw new IllegalStateException("must start!");
}
stop();
for(SearchListener listener : searchListeners) {
listener.searchStarted(CoreSearch.this);
}
doSearch(false);
}
@Override
public void stop() {
if(!processingStarted.compareAndSet(true, true)) {
return;
}
if (torrentWebSearch != null) {
torrentWebSearch.stop();
}
searchEventBroadcaster.broadcast(new SearchEvent(this, SearchEvent.Type.STOPPED));
listenerList.removeQueryReplyListener(searchGuid, qrListener);
searchServices.stopQuery(new GUID(searchGuid));
for(SearchListener listener : searchListeners) {
listener.searchStopped(CoreSearch.this);
}
}
public GUID getQueryGuid() {
return new GUID(searchGuid);
}
@Override
public void handleSearchResult(Search search, SearchResult searchResult) {
for (SearchListener listener : searchListeners) {
listener.handleSearchResult(this, searchResult);
}
}
@Override
public void handleSearchResults(Search search, Collection<? extends SearchResult> searchResults) {
for (SearchListener listener : searchListeners) {
listener.handleSearchResults(this, searchResults);
}
}
@Override
public void searchStarted(Search search) {
}
@Override
public void searchStopped(Search search) {
}
private class QrListener implements QueryReplyListener {
@Override
public void handleQueryReply(RemoteFileDesc rfd, QueryReply queryReply,
Set<? extends IpPort> locs) {
RemoteFileDescAdapter rfdAdapter = remoteFileDescAdapterFactory.create(rfd, locs);
handleSearchResult(CoreSearch.this, rfdAdapter);
}
}
private class FriendSearchListenerImpl implements FriendSearchListener {
public void handleFriendResults(Collection<SearchResult> results) {
handleSearchResults(CoreSearch.this, results);
}
}
}