package org.limewire.core.impl.search; import java.util.Arrays; import java.util.Collection; import java.util.List; 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.api.search.sponsored.SponsoredResult; import org.limewire.core.api.search.sponsored.SponsoredResultTarget; import org.limewire.core.impl.library.FriendSearcher; import org.limewire.core.impl.search.sponsored.CoreSponsoredResult; import org.limewire.core.settings.PromotionSettings; import org.limewire.geocode.GeocodeInformation; import org.limewire.io.GUID; import org.limewire.io.IpPort; import org.limewire.listener.EventBroadcaster; import org.limewire.promotion.PromotionSearcher; import org.limewire.promotion.PromotionSearcher.PromotionSearchResultsCallback; import org.limewire.promotion.containers.PromotionMessageContainer; import org.limewire.util.Clock; import com.google.inject.Inject; import com.google.inject.Provider; 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 { private final SearchDetails searchDetails; private final SearchServices searchServices; private final QueryReplyListenerList listenerList; private final PromotionSearcher promotionSearcher; private final FriendSearcher friendSearcher; private final Provider<GeocodeInformation> geoLocation; /** * 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 Clock clock; private final AdvancedQueryStringBuilder compositeQueryBuilder; /** * The guid of the last active search. */ volatile byte[] searchGuid; @Inject public CoreSearch(@Assisted SearchDetails searchDetails, SearchServices searchServices, QueryReplyListenerList listenerList, PromotionSearcher promotionSearcher, FriendSearcher friendSearcher, Provider<GeocodeInformation> geoLocation, @Named("backgroundExecutor") ScheduledExecutorService backgroundExecutor, EventBroadcaster<SearchEvent> searchEventBroadcaster, LimeXMLDocumentFactory xmlDocumentFactory, Clock clock, AdvancedQueryStringBuilder compositeQueryBuilder) { this.searchDetails = searchDetails; this.searchServices = searchServices; this.listenerList = listenerList; this.promotionSearcher = promotionSearcher; this.friendSearcher = friendSearcher; this.geoLocation = geoLocation; this.backgroundExecutor = backgroundExecutor; this.searchEventBroadcaster = searchEventBroadcaster; this.clock = clock; this.compositeQueryBuilder = compositeQueryBuilder; } @Override public SearchCategory getCategory() { return searchDetails.getSearchCategory(); } @Override public String getQuery() { return searchDetails.getSearchQuery(); } @Override public void addSearchListener(SearchListener searchListener) { searchListeners.add(searchListener); } @Override public void removeSearchListener(SearchListener searchListener) { searchListeners.remove(searchListener); } @Override public CopyOnWriteArrayList<SearchListener> getListenerList() { // TODO Auto-generated method stub return this.searchListeners; } @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, MediaTypeConverter.toMediaType(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()); } searchServices.query(searchGuid, query, advancedQuery, MediaTypeConverter.toMediaType(searchDetails.getSearchCategory())); backgroundExecutor.execute(new Runnable() { @Override public void run() { friendSearcher.doSearch(searchDetails, friendSearchListener); } }); if (initial && PromotionSettings.PROMOTION_SYSTEM_IS_ENABLED.getValue() && promotionSearcher.isEnabled()) { final PromotionSearchResultsCallback callback = new PromotionSearchResultsCallback() { @Override public void process(PromotionMessageContainer result) { SponsoredResultTarget target; if(result.getOptions().isOpenInStoreTab()) { target = SponsoredResultTarget.STORE; } else if(result.getOptions().isOpenInHomeTab()) { target = SponsoredResultTarget.HOME; } else { target = SponsoredResultTarget.EXTERNAL; } String title = result.getTitle(); String displayUrl = result.getDisplayUrl(); if(displayUrl.isEmpty()) { displayUrl = SearchUrlUtils.stripUrl(result.getURL()); } if(title.isEmpty()) { title = displayUrl; } CoreSponsoredResult coreSponsoredResult = new CoreSponsoredResult( title, result.getDescription(), displayUrl, SearchUrlUtils.createPromotionUrl(result, clock.now() / 1000), target); handleSponsoredResults(coreSponsoredResult); } }; final String finalQuery = query; backgroundExecutor.execute(new Runnable() { @Override public void run() { promotionSearcher.search(finalQuery, callback, geoLocation.get()); } }); } } /** * 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; } 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); } } @Override public GUID getQueryGuid() { return new GUID(searchGuid); } private void handleSponsoredResults(SponsoredResult... sponsoredResults) { List<SponsoredResult> resultList = Arrays.asList(sponsoredResults); for(SearchListener listener : searchListeners) { listener.handleSponsoredResults(CoreSearch.this, resultList); } } private class QrListener implements QueryReplyListener { @Override public void handleQueryReply(RemoteFileDesc rfd, QueryReply queryReply, Set<? extends IpPort> locs) { RemoteFileDescAdapter rfdAdapter = new RemoteFileDescAdapter(rfd, locs); for (SearchListener listener : searchListeners) { listener.handleSearchResult(CoreSearch.this, rfdAdapter); } } } private class FriendSearchListenerImpl implements FriendSearchListener { public void handleFriendResults(Collection<SearchResult> results) { for (SearchListener listener : searchListeners) { listener.handleSearchResults(CoreSearch.this, results); } } } }