package com.limegroup.gnutella.gui.search;
import java.io.File;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import javax.swing.JComponent;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import com.limegroup.gnutella.BrowseHostHandler;
import com.limegroup.gnutella.Downloader;
import com.limegroup.gnutella.Endpoint;
import com.limegroup.gnutella.GUID;
import com.limegroup.gnutella.MediaType;
import com.limegroup.gnutella.RemoteFileDesc;
import com.limegroup.gnutella.RouterService;
import com.limegroup.gnutella.gui.GUIMediator;
import com.limegroup.gnutella.gui.download.DownloaderFactory;
import com.limegroup.gnutella.gui.download.DownloaderUtils;
import com.limegroup.gnutella.gui.download.SearchResultDownloaderFactory;
import com.limegroup.gnutella.search.HostData;
import com.limegroup.gnutella.settings.FileSetting;
import com.limegroup.gnutella.settings.QuestionsHandler;
import com.limegroup.gnutella.settings.SearchSettings;
import com.limegroup.gnutella.settings.SharingSettings;
import com.limegroup.gnutella.util.I18NConvert;
import com.limegroup.gnutella.util.IpPort;
import com.limegroup.gnutella.util.StringUtils;
import com.limegroup.gnutella.xml.LimeXMLDocument;
import com.limegroup.gnutella.xml.LimeXMLProperties;
/**
* This class acts as a mediator between the various search components --
* the hub that all traffic passes through. This allows the decoupling of
* the various search packages and simplfies the responsibilities of the
* underlying classes.
*/
public final class SearchMediator {
/**
* Query text is valid.
*/
public static final int QUERY_VALID = 0;
/**
* Query text is empty.
*/
public static final int QUERY_EMPTY = 1;
/**
* Query text is too short.
*/
public static final int QUERY_TOO_SHORT = 2;
/**
* Query text is too long.
*/
public static final int QUERY_TOO_LONG = 3;
/**
* Query xml is too long.
*/
public static final int QUERY_XML_TOO_LONG = 4;
/**
* Query contains invalid characters.
*/
public static final int QUERY_INVALID_CHARACTERS = 5;
static final String DOWNLOAD_STRING =
GUIMediator.getStringResource("SEARCH_DOWNLOAD_BUTTON_LABEL");
static final String KILL_STRING =
GUIMediator.getStringResource("SEARCH_PUBLIC_KILL_STRING");
static final String STOP_STRING =
GUIMediator.getStringResource("SEARCH_PUBLIC_STOP_STRING");
static final String LAUNCH_STRING =
GUIMediator.getStringResource("SEARCH_PUBLIC_LAUNCH_STRING");
static final String BROWSE_STRING =
GUIMediator.getStringResource("SEARCH_PUBLIC_BROWSE_STRING");
static final String CHAT_STRING =
GUIMediator.getStringResource("SEARCH_PUBLIC_CHAT_STRING");
static final String REPEAT_SEARCH_STRING =
GUIMediator.getStringResource("SEARCH_PUBLIC_REPEAT_SEARCH_STRING");
static final String BROWSE_HOST_STRING =
GUIMediator.getStringResource("SEARCH_PUBLIC_BROWSE_STRING");
static final String BITZI_LOOKUP_STRING =
GUIMediator.getStringResource("SEARCH_PUBLIC_BITZI_LOOKUP_STRING");
static final String BLOCK_STRING =
GUIMediator.getStringResource("SEARCH_PUBLIC_BLOCK_STRING");
static final String MARK_AS_STRING =
GUIMediator.getStringResource("SEARCH_SPAM_MARK_AS_LABEL");
static final String SPAM_STRING =
GUIMediator.getStringResource("SEARCH_SPAM_BUTTON_LABEL");
static final String NOT_SPAM_STRING =
GUIMediator.getStringResource("SEARCH_NOT_SPAM_BUTTON_LABEL");
/** A name of attribute, which holds a query in state of downloaded file. */
public static final String SEARCH_INFORMATION_KEY = "searchInformationMap";
/**
* Variable for the component that handles all search input from the user.
*/
private static final SearchInputManager INPUT_MANAGER =
new SearchInputManager();
/**
* This instance handles the display of all search results.
*/
private static final SearchResultDisplayer RESULT_DISPLAYER =
new SearchResultDisplayer();
/**
* Constructs the UI components of the search result display area of the
* search tab.
*/
public SearchMediator() {
// Set the splash screen text...
final String splashScreenString =
GUIMediator.getStringResource("SPLASH_STATUS_SEARCH_WINDOW");
GUIMediator.setSplashScreenString(splashScreenString);
GUIMediator.addRefreshListener(RESULT_DISPLAYER);
// Link up the tabs of results with the filters of the input screen.
RESULT_DISPLAYER.setSearchListener(new ChangeListener() {
public void stateChanged(ChangeEvent e) {
ResultPanel panel = RESULT_DISPLAYER.getSelectedResultPanel();
if(panel == null)
INPUT_MANAGER.clearFilters();
else
INPUT_MANAGER.setFiltersFor(panel);
}
});
}
/**
* Rebuilds the INPUT_MANAGER's panel.
*/
public static void rebuildInputPanel() {
INPUT_MANAGER.rebuild();
}
/**
* Notification that the address has changed -- pass it along.
*/
public static void addressChanged() {
INPUT_MANAGER.addressChanged();
}
/**
* Informs the INPUT_MANAGER that we want to display the searching
* window.
*/
public static void showSearchInput() {
INPUT_MANAGER.goToSearch();
}
/**
* Requests the search focus in the INPUT_MANAGER.
*/
public static void requestSearchFocus() {
INPUT_MANAGER.requestSearchFocus();
}
/**
* Updates all current results.
*/
public static void updateResults() {
RESULT_DISPLAYER.updateResults();
}
/**
* Repeats the given search.
*/
static byte[] repeatSearch(ResultPanel rp, SearchInformation info) {
if(!validate(info))
return null;
// 1. Update panel with new GUID
byte [] guidBytes = RouterService.newQueryGUID();
final GUID newGuid = new GUID(guidBytes);
RouterService.stopQuery(new GUID(rp.getGUID()));
rp.setGUID(newGuid);
INPUT_MANAGER.panelReset(rp);
if(info.isBrowseHostSearch()) {
IpPort ipport = info.getIpPort();
String host = ipport.getAddress();
int port = ipport.getPort();
if(host != null && port != 0) {
GUIMediator.instance().setSearching(true);
reBrowseHost(host, port, rp);
}
} else {
GUIMediator.instance().setSearching(true);
doSearch(guidBytes, info);
}
return guidBytes;
}
/**
* Browses the first selected host. Fails silently if couldn't browse.
*/
static void doBrowseHost(ResultPanel rp) {
TableLine line = rp.getSelectedLine();
if(line == null)
return;
// Get the browse-host RFD from the line.
RemoteFileDesc rfd = line.getBrowseHostEnabledRFD();
if(rfd == null)
return;
// See if it is firewalled
byte[] serventIDBytes = rfd.getClientGUID();
// if the reply is to a multicast query, don't use any
// push proxies so we definitely will send a UDP push request
final Set proxies = rfd.isReplyToMulticast() ?
Collections.EMPTY_SET : rfd.getPushProxies();
final GUID serventID = new GUID(serventIDBytes);
// Get the host's address....
final String host = rfd.getHost();
final int port = rfd.getPort();
doBrowseHost2(host, port, serventID, proxies, rfd.supportsFWTransfer());
}
/**
* Allows for browsing of a host from outside of the search package.
*/
public static void doBrowseHost(final RemoteFileDesc rfd) {
doBrowseHost2(rfd.getHost(), rfd.getPort(),
new GUID(rfd.getClientGUID()), rfd.getPushProxies(),
rfd.supportsFWTransfer());
}
/**
* Allows for browsing of a host from outside of the search package
* without an rfd.
*/
public static void doBrowseHost(final String host, final int port,
final GUID guid) {
if (guid == null)
doBrowseHost2(host, port, null, null, false);
else
doBrowseHost2(host, port, new GUID(guid.bytes()), null, false);
}
/**
* Re-browses the host. Fails silently if browse failed...
* TODO: WILL NOT WORK FOR RE-BROWSES THAT REQUIRES A PUSH!!!
*/
private static void reBrowseHost(final String host, final int port,
ResultPanel in) {
// Update the GUID
final GUID guid = new GUID(GUID.makeGuid());
in.setGUID(guid);
BrowseHostHandler bhh =
RouterService.doAsynchronousBrowseHost(host, port, guid,
new GUID(GUID.makeGuid()),
null, false);
in.setBrowseHostHandler(bhh);
INPUT_MANAGER.panelReset(in);
}
/**
* Browses the passed host at the passed port.
* Fails silently if couldn't browse.
* @param host The host to browse
* @param port The port at which to browse
*/
static private void doBrowseHost2(String host, int port,
GUID serventID,
Set proxies, boolean canDoFWTransfer) {
// Update the GUI
GUID guid = new GUID(GUID.makeGuid());
ResultPanel rp = addBrowseHostTab(guid, host + ":" + port);
// Do the actual browse host
BrowseHostHandler bhh = RouterService.doAsynchronousBrowseHost(
host, port, guid, serventID, proxies,
canDoFWTransfer);
rp.setBrowseHostHandler(bhh);
}
/**
* Call this when a Browse Host fails.
* @param guid The guid associated with this Browse.
*/
public static void browseHostFailed(GUID guid) {
RESULT_DISPLAYER.browseHostFailed(guid);
}
/**
* Initiates a new search with the specified SearchInformation.
*
* Returns the GUID of the search if a search was initiated,
* otherwise returns null.
*/
public static byte[] triggerSearch(SearchInformation info) {
if(!validate(info))
return null;
// generate a guid for the search.
byte[] guid = RouterService.newQueryGUID();
// only add tab if this isn't a browse-host search.
if(!info.isBrowseHostSearch()) {
addResultTab(new GUID(guid), info);
}
if(info.isKeywordSearch())
GUIMediator.instance().checkForJavaVersion();
doSearch(guid, info);
return guid;
}
/**
* Triggers a search given the text in the search field. For testing
* purposes returns the 16-byte GUID of the search or null if the search
* didn't happen because it was greedy, etc.
*/
public static byte[] triggerSearch(String query) {
return triggerSearch(
SearchInformation.createKeywordSearch(query, null,
MediaType.getAnyTypeMediaType())
);
}
/**
* Validates the given search information.
*/
private static boolean validate(SearchInformation info) {
switch (validateInfo(info)) {
case QUERY_EMPTY:
return false;
case QUERY_TOO_SHORT:
GUIMediator.showError("ERROR_THREE_CHARACTER_SEARCH");
return false;
case QUERY_TOO_LONG:
String xml = info.getXML();
if (xml == null || xml.length() == 0) {
GUIMediator.showError("ERROR_SEARCH_TOO_LARGE");
}
else {
GUIMediator.showError("ERROR_XML_SEARCH_TOO_LARGE");
}
return false;
case QUERY_XML_TOO_LONG:
GUIMediator.showError("ERROR_XML_SEARCH_TOO_LARGE");
return false;
case QUERY_VALID:
default:
// only show search messages if not doing browse host.
if(!info.isBrowseHostSearch()) {
if(!RouterService.isConnected()) {
// if not connected or connecting, show one message.
if(!RouterService.isConnecting())
GUIMediator.showMessage("SEARCH_NOT_CONNECTED", QuestionsHandler.NO_NOT_CONNECTED);
else // if attempting to connect, show another.
GUIMediator.showMessage("SEARCH_STILL_CONNECTING", QuestionsHandler.NO_STILL_CONNECTING);
}
}
return true;
}
}
/**
* Validates the a search info and returns {@link #QUERY_VALID} if it is
* valid.
* @param info
* @return one of the static <code>QUERY*</code> fields
*/
public static int validateInfo(SearchInformation info) {
String query = info.getQuery();
String xml = info.getXML();
if (query.length() == 0) {
return QUERY_EMPTY;
}
else if (query.length() <= 2 && !(query.length() == 2 &&
((Character.isDigit(query.charAt(0)) &&
Character.isLetter(query.charAt(1))) ||
(Character.isLetter(query.charAt(0)) &&
Character.isDigit(query.charAt(1)))))) {
return QUERY_TOO_SHORT;
}
else if (query.length() > SearchSettings.MAX_QUERY_LENGTH.getValue()
|| (I18NConvert.instance().getNorm(query).length()
> SearchSettings.MAX_QUERY_LENGTH.getValue())) {
return QUERY_TOO_LONG;
}
else if (xml != null
&& xml.length() > SearchSettings.MAX_XML_QUERY_LENGTH.getValue()) {
return QUERY_XML_TOO_LONG;
}
if (StringUtils.containsCharacters(query,SearchSettings.ILLEGAL_CHARS.getValue()))
return QUERY_INVALID_CHARACTERS;
return QUERY_VALID;
}
/**
* Does the actual search.
*/
private static void doSearch(byte[] guid, SearchInformation info) {
String query = info.getQuery();
String xml = info.getXML();
MediaType media = info.getMediaType();
if(info.isXMLSearch()) {
RouterService.query(guid, query, xml, media);
} else if(info.isKeywordSearch()) {
RouterService.query(guid, query, media);
} else if(info.isWhatsNewSearch()) {
RouterService.queryWhatIsNew(guid, media);
} else if(info.isBrowseHostSearch()) {
IpPort ipport = info.getIpPort();
doBrowseHost(ipport.getAddress(), ipport.getPort(), null);
}
}
/**
* Adds a single result tab for the specified GUID, type,
* standard query string, and XML query string.
*/
private static ResultPanel addResultTab(GUID guid,
SearchInformation info) {
return RESULT_DISPLAYER.addResultTab(guid, info);
}
/**
* Adds a browse host tab with the given description.
*/
private static ResultPanel addBrowseHostTab(GUID guid, String desc) {
return RESULT_DISPLAYER.addResultTab(guid,
SearchInformation.createBrowseHostSearch(desc)
);
}
/**
* If i rp is no longer the i'th panel of this, returns silently.
* Otherwise adds line to rp under the given group. Updates the count
* on the tab in this and restarts the spinning lime.
* @requires this is called from Swing thread
* @modifies this
*/
public static void handleQueryResult(RemoteFileDesc rfd,
HostData data,
Set alts) {
byte[] replyGUID = data.getMessageGUID();
ResultPanel rp = getResultPanelForGUID(new GUID(replyGUID));
if(rp != null) {
SearchResult sr = new SearchResult(rfd, data, alts);
RESULT_DISPLAYER.addQueryResult(replyGUID, sr, rp);
}
}
/**
* Downloads the selected files in the currently displayed
* <tt>ResultPanel</tt> if there is one.
*/
static void doDownload(final ResultPanel rp) {
final TableLine[] lines = rp.getAllSelectedLines();
SwingUtilities.invokeLater(new Runnable() {
public void run() {
SearchMediator.downloadAll(lines, new GUID(rp.getGUID()),
rp.getSearchInformation());
rp.refresh();
}
});
}
/**
* Opens a dialog where you can specify the download directory and final
* filename for the selected file.
* @param panel
* @throws IllegalStateException when there is more than one file selected
* for download or there is no file selected.
*/
static void doDownloadAs(final ResultPanel panel) {
final TableLine[] lines = panel.getAllSelectedLines();
if (lines.length != 1) {
throw new IllegalStateException("There should only be one search result selected");
}
downloadLine(lines[0], new GUID(panel.getGUID()), null, null, true,
panel.getSearchInformation());
}
/**
* Downloads all the selected lines.
*/
private static void downloadAll(TableLine[] lines, GUID guid,
SearchInformation searchInfo)
{
for(int i = 0; i < lines.length; i++)
downloadLine(lines[i], guid, null, null, false, searchInfo);
}
/**
* Downloads the given TableLine.
* @param line
* @param guid
* @param saveDir optionally the directory where the final file should be
* saved to, can be <code>null</code>
* @param fileName the optional filename of the final file, can be
* <code>null</code>
* @param searchInfo The query used to find the file being downloaded.
*/
private static void downloadLine(TableLine line, GUID guid, File saveDir,
String fileName, boolean saveAs, SearchInformation searchInfo)
{
if (line == null)
throw new NullPointerException("Tried to download null line");
// do not download if no license and user does not acknowledge
if (!line.isLicenseAvailable() && !GUIMediator.showFirstDownloadDialog())
return;
RemoteFileDesc[] rfds;
Set /* of EndpointPoint */ alts = new HashSet();
List /* of RemoteFileDesc */ otherRFDs = new LinkedList();
rfds = line.getAllRemoteFileDescs();
alts.addAll(line.getAlts());
// Iterate through RFDs and remove matching alts.
// Also store the first SHA1 capable RFD for collecting alts.
RemoteFileDesc sha1RFD = null;
for(int i = 0; i < rfds.length; i++) {
RemoteFileDesc next = rfds[i];
// this has been moved down until the download is actually started
// next.setDownloading(true);
next.setRetryAfter(0);
if(next.getSHA1Urn() != null)
sha1RFD = next;
alts.remove(new Endpoint(next.getHost(), next.getPort()));
}
// If no SHA1 rfd, just use the first.
if(sha1RFD == null)
sha1RFD = rfds[0];
// Now iterate through alts & add more rfds.
for(Iterator i = alts.iterator(); i.hasNext(); ) {
Endpoint next = (Endpoint)i.next();
otherRFDs.add(new RemoteFileDesc(sha1RFD, next));
}
// determine per mediatype directory if saveLocation == null
// and only pass it through if directory is different from default
// save directory == !isDefault()
if (saveDir == null && line.getNamedMediaType() != null) {
FileSetting fs = SharingSettings.getFileSettingForMediaType
(line.getNamedMediaType().getMediaType());
if (!fs.isDefault()) {
saveDir = fs.getValue();
}
}
downloadWithOverwritePrompt(rfds, otherRFDs, guid, saveDir, fileName,
saveAs, searchInfo);
}
/**
* Downloads the given files, prompting the user if the file already exists.
* @param queryGUID the guid of the query you ar downloading rfds for.
* @param searchInfo The query used to find the file being downloaded.
*/
private static void downloadWithOverwritePrompt(RemoteFileDesc[] rfds,
List alts, GUID queryGUID,
File saveDir, String fileName,
boolean saveAs,
SearchInformation searchInfo)
{
if (rfds.length < 1)
return;
if (containsExe(rfds)) {
if (!userWantsExeDownload())
return;
}
// Before proceeding...check if there is an rfd withpure metadata
// ie no file
int actLine = 0;
boolean pureFound = false;
for (; actLine < rfds.length; actLine++) {
if (rfds[actLine].getIndex() ==
LimeXMLProperties.DEFAULT_NONFILE_INDEX) {
// we have our line
pureFound = true;
break;
}
}
if (pureFound) {
LimeXMLDocument doc = rfds[actLine].getXMLDocument();
String action = doc.getAction();
if (action != null && !action.equals("")) { // valid action
GUIMediator.openURL(action);
return; // goodbye
}
}
// No pure metadata lines found...continue as usual...
DownloaderFactory factory = new SearchResultDownloaderFactory
(rfds, alts, queryGUID, saveDir, fileName);
Downloader dl = saveAs ? DownloaderUtils.createDownloaderAs(factory)
: DownloaderUtils.createDownloader(factory);
if (dl != null) {
setAsDownloading(rfds);
if (validateInfo(searchInfo) == QUERY_VALID)
dl.setAttribute(SEARCH_INFORMATION_KEY, searchInfo.toMap());
}
}
private static void setAsDownloading(RemoteFileDesc[] rfds) {
for (int i = 0; i < rfds.length; i++) {
rfds[i].setDownloading(true);
}
}
/**
* Returns true if any of the entries of rfd contains a .exe file.
*/
private static boolean containsExe(RemoteFileDesc[] rfd) {
for (int i = 0; i < rfd.length; i++) {
if (rfd[i].getFileName().toLowerCase(Locale.US).endsWith("exe"))
return true;
}
return false;
}
/**
* Prompts the user if they want to download an .exe file.
* Returns true s/he said yes.
*/
private static boolean userWantsExeDownload() {
String middleMsg = GUIMediator.getStringResource("SEARCH_VIRUS_MSG_TWO");
int response = GUIMediator.showYesNoMessage("SEARCH_VIRUS_MSG_ONE",
middleMsg,
"SEARCH_VIRUS_MSG_THREE",
QuestionsHandler.PROMPT_FOR_EXE);
return response == GUIMediator.YES_OPTION;
}
////////////////////////// Other Controls ///////////////////////////
/**
* called by ResultPanel when the views are changed. Used to set the
* tab to indicate the correct number of TableLines in the current
* view.
*/
static void setTabDisplayCount(ResultPanel rp) {
RESULT_DISPLAYER.setTabDisplayCount(rp);
}
/**
* @modifies tabbed pane, entries
* @effects removes the currently selected result window (if any)
* from this
*/
static void killSearch() {
RESULT_DISPLAYER.killSearch();
}
/**
* Notification that a given ResultPanel has been selected
*/
static void panelSelected(ResultPanel panel) {
INPUT_MANAGER.setFiltersFor(panel);
}
/**
* Notification that a search has been killed.
*/
static void searchKilled(ResultPanel panel) {
INPUT_MANAGER.panelRemoved(panel);
ResultPanel rp = RESULT_DISPLAYER.getSelectedResultPanel();
if(rp != null)
INPUT_MANAGER.setFiltersFor(rp);
}
/**
* Checks to see if the spinning lime should be stopped.
*/
static void checkToStopLime() {
RESULT_DISPLAYER.checkToStopLime();
}
/**
* Returns the <tt>ResultPanel</tt> for the specified GUID.
*
* @param rguid the guid to search for
* @return the <tt>ResultPanel</tt> that matches the GUID, or null
* if none match.
*/
static ResultPanel getResultPanelForGUID(GUID rguid) {
return RESULT_DISPLAYER.getResultPanelForGUID(rguid);
}
/** @returns true if the user is still using the query results for the input
* guid, else false.
*/
public static boolean queryIsAlive(GUID guid) {
return (getResultPanelForGUID(guid) != null);
}
/**
* Returns the search input panel component.
*
* @return the search input panel component
*/
public static JComponent getSearchComponent() {
return INPUT_MANAGER.getComponent();
}
/**
* Returns the <tt>JComponent</tt> instance containing all of the
* search result UI components.
*
* @return the <tt>JComponent</tt> instance containing all of the
* search result UI components
*/
public static JComponent getResultComponent() {
return RESULT_DISPLAYER.getComponent();
}
}