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(); } }