home *** CD-ROM | disk | FTP | other *** search
/ PC World 2008 February / PCWorld_2008-02_cd.bin / temacd / songbird / Songbird_0.4_windows-i686.exe / components / nsSearchSuggestions.js < prev    next >
Text File  |  2007-12-21  |  24KB  |  736 lines

  1. /* ***** BEGIN LICENSE BLOCK *****
  2.  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  3.  *
  4.  * The contents of this file are subject to the Mozilla Public License Version
  5.  * 1.1 (the "License"); you may not use this file except in compliance with
  6.  * the License. You may obtain a copy of the License at
  7.  * http://www.mozilla.org/MPL/
  8.  *
  9.  * Software distributed under the License is distributed on an "AS IS" basis,
  10.  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  11.  * for the specific language governing rights and limitations under the
  12.  * License.
  13.  *
  14.  * The Original Code is Google Suggest Autocomplete Implementation for Firefox.
  15.  *
  16.  * The Initial Developer of the Original Code is Google Inc.
  17.  * Portions created by the Initial Developer are Copyright (C) 2006
  18.  * the Initial Developer. All Rights Reserved.
  19.  *
  20.  * Contributor(s):
  21.  *   Ben Goodger <beng@google.com>
  22.  *   Mike Connor <mconnor@mozilla.com>
  23.  *   Joe Hughes  <joe@retrovirus.com>
  24.  *   Pamela Greene <pamg.bugs@gmail.com>
  25.  *
  26.  * Alternatively, the contents of this file may be used under the terms of
  27.  * either the GNU General Public License Version 2 or later (the "GPL"), or
  28.  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  29.  * in which case the provisions of the GPL or the LGPL are applicable instead
  30.  * of those above. If you wish to allow use of your version of this file only
  31.  * under the terms of either the GPL or the LGPL, and not to allow others to
  32.  * use your version of this file under the terms of the MPL, indicate your
  33.  * decision by deleting the provisions above and replace them with the notice
  34.  * and other provisions required by the GPL or the LGPL. If you do not delete
  35.  * the provisions above, a recipient may use your version of this file under
  36.  * the terms of any one of the MPL, the GPL or the LGPL.
  37.  *
  38.  * ***** END LICENSE BLOCK ***** */
  39.  
  40. const SEARCH_RESPONSE_SUGGESTION_JSON = "application/x-suggestions+json";
  41.  
  42. const BROWSER_SUGGEST_PREF = "browser.search.suggest.enabled";
  43. const XPCOM_SHUTDOWN_TOPIC              = "xpcom-shutdown";
  44. const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID = "nsPref:changed";
  45. const SEARCH_BUNDLE = "chrome://browser/locale/search.properties";
  46.  
  47. const Cc = Components.classes;
  48. const Ci = Components.interfaces;
  49. const Cr = Components.results;
  50. const Cu = Components.utils;
  51.  
  52. const HTTP_OK                    = 200;
  53. const HTTP_INTERNAL_SERVER_ERROR = 500;
  54. const HTTP_BAD_GATEWAY           = 502;
  55. const HTTP_SERVICE_UNAVAILABLE   = 503;
  56.  
  57. Cu.import("resource://gre/modules/XPCOMUtils.jsm");
  58. Cu.import("resource://gre/modules/JSON.jsm");
  59.  
  60. /**
  61.  * SuggestAutoCompleteResult contains the results returned by the Suggest
  62.  * service - it implements nsIAutoCompleteResult and is used by the auto-
  63.  * complete controller to populate the front end.
  64.  * @constructor
  65.  */
  66. function SuggestAutoCompleteResult(searchString,
  67.                                    searchResult,
  68.                                    defaultIndex,
  69.                                    errorDescription,
  70.                                    results,
  71.                                    comments,
  72.                                    formHistoryResult) {
  73.   this._searchString = searchString;
  74.   this._searchResult = searchResult;
  75.   this._defaultIndex = defaultIndex;
  76.   this._errorDescription = errorDescription;
  77.   this._results = results;
  78.   this._comments = comments;
  79.   this._formHistoryResult = formHistoryResult;
  80. }
  81. SuggestAutoCompleteResult.prototype = {
  82.   /**
  83.    * The user's query string
  84.    * @private
  85.    */
  86.   _searchString: "",
  87.  
  88.   /**
  89.    * The result code of this result object, see |get searchResult| for possible
  90.    * values.
  91.    * @private
  92.    */
  93.   _searchResult: 0,
  94.  
  95.   /**
  96.    * The default item that should be entered if none is selected
  97.    * @private
  98.    */
  99.   _defaultIndex: 0,
  100.  
  101.   /**
  102.    * The reason the search failed
  103.    * @private
  104.    */
  105.   _errorDescription: "",
  106.  
  107.   /**
  108.    * The list of words returned by the Suggest Service
  109.    * @private
  110.    */
  111.   _results: [],
  112.  
  113.   /**
  114.    * The list of Comments (number of results - or page titles) returned by the
  115.    * Suggest Service.
  116.    * @private
  117.    */
  118.   _comments: [],
  119.  
  120.   /**
  121.    * A reference to the form history nsIAutocompleteResult that we're wrapping.
  122.    * We use this to forward removeEntryAt calls as needed.
  123.    */
  124.   _formHistoryResult: null,
  125.  
  126.   /**
  127.    * @return the user's query string
  128.    */
  129.   get searchString() {
  130.     return this._searchString;
  131.   },
  132.  
  133.   /**
  134.    * @return the result code of this result object, either:
  135.    *         RESULT_IGNORED   (invalid searchString)
  136.    *         RESULT_FAILURE   (failure)
  137.    *         RESULT_NOMATCH   (no matches found)
  138.    *         RESULT_SUCCESS   (matches found)
  139.    */
  140.   get searchResult() {
  141.     return this._searchResult;
  142.   },
  143.  
  144.   /**
  145.    * @return the default item that should be entered if none is selected
  146.    */
  147.   get defaultIndex() {
  148.     return this._defaultIndex;
  149.   },
  150.  
  151.   /**
  152.    * @return the reason the search failed
  153.    */
  154.   get errorDescription() {
  155.     return this._errorDescription;
  156.   },
  157.  
  158.   /**
  159.    * @return the number of results
  160.    */
  161.   get matchCount() {
  162.     return this._results.length;
  163.   },
  164.  
  165.   /**
  166.    * Retrieves a result
  167.    * @param  index    the index of the result requested
  168.    * @return          the result at the specified index
  169.    */
  170.   getValueAt: function(index) {
  171.     return this._results[index];
  172.   },
  173.  
  174.   /**
  175.    * Retrieves a comment (metadata instance)
  176.    * @param  index    the index of the comment requested
  177.    * @return          the comment at the specified index
  178.    */
  179.   getCommentAt: function(index) {
  180.     return this._comments[index];
  181.   },
  182.  
  183.   /**
  184.    * Retrieves a style hint specific to a particular index.
  185.    * @param  index    the index of the style hint requested
  186.    * @return          the style hint at the specified index
  187.    */
  188.   getStyleAt: function(index) {
  189.     if (!this._comments[index])
  190.       return null;  // not a category label, so no special styling
  191.  
  192.     if (index == 0)
  193.       return "suggestfirst";  // category label on first line of results
  194.  
  195.     return "suggesthint";   // category label on any other line of results
  196.   },
  197.  
  198.   /**
  199.    * Retrieves an image url.
  200.    * @param  index    the index of the image url requested
  201.    * @return          the image url at the specified index
  202.    */
  203.   getImageAt: function(index) {
  204.     return "";
  205.   },
  206.  
  207.   /**
  208.    * Removes a result from the resultset
  209.    * @param  index    the index of the result to remove
  210.    */
  211.   removeValueAt: function(index, removeFromDatabase) {
  212.     // Forward the removeValueAt call to the underlying result if we have one
  213.     // Note: this assumes that the form history results were added to the top
  214.     // of our arrays.
  215.     if (removeFromDatabase && this._formHistoryResult &&
  216.         index < this._formHistoryResult.matchCount) {
  217.       // Delete the history result from the DB
  218.       this._formHistoryResult.removeValueAt(index, true);
  219.     }
  220.     this._results.splice(index, 1);
  221.     this._comments.splice(index, 1);
  222.   },
  223.  
  224.   // nsISupports
  225.   QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompleteResult])
  226. };
  227.  
  228. /**
  229.  * SuggestAutoComplete is a base class that implements nsIAutoCompleteSearch
  230.  * and can collect results for a given search by using the search URL supplied
  231.  * by the subclass. We do it this way since the AutoCompleteController in
  232.  * Mozilla requires a unique XPCOM Service for every search provider, even if
  233.  * the logic for two providers is identical.
  234.  * @constructor
  235.  */
  236. function SuggestAutoComplete() {
  237.   this._init();
  238. }
  239. SuggestAutoComplete.prototype = {
  240.  
  241.   _init: function() {
  242.     this._addObservers();
  243.     this._loadSuggestPref();
  244.   },
  245.  
  246.   /**
  247.    * this._strings is the string bundle for message internationalization.
  248.    */
  249.   get _strings() {
  250.     if (!this.__strings) {
  251.       var sbs = Cc["@mozilla.org/intl/stringbundle;1"].
  252.                 getService(Ci.nsIStringBundleService);
  253.  
  254.       this.__strings = sbs.createBundle(SEARCH_BUNDLE);
  255.     }
  256.     return this.__strings;
  257.   },
  258.   __strings: null,
  259.  
  260.   /**
  261.    * Search suggestions will be shown if this._suggestEnabled is true.
  262.    */
  263.   _loadSuggestPref: function SAC_loadSuggestPref() {
  264.     var prefService = Cc["@mozilla.org/preferences-service;1"].
  265.                       getService(Ci.nsIPrefBranch);
  266.     this._suggestEnabled = prefService.getBoolPref(BROWSER_SUGGEST_PREF);
  267.   },
  268.   _suggestEnabled: null,
  269.  
  270.   /*************************************************************************
  271.    * Server request backoff implementation fields below
  272.    * These allow us to throttle requests if the server is getting hammered.
  273.    **************************************************************************/
  274.  
  275.   /**
  276.    * This is an array that contains the timestamps (in unixtime) of
  277.    * the last few backoff-triggering errors.
  278.    */
  279.   _serverErrorLog: [],
  280.  
  281.   /**
  282.    * If we receive this number of backoff errors within the amount of time
  283.    * specified by _serverErrorPeriod, then we initiate backoff.
  284.    */
  285.   _maxErrorsBeforeBackoff: 3,
  286.  
  287.   /**
  288.    * If we receive enough consecutive errors (where "enough" is defined by
  289.    * _maxErrorsBeforeBackoff above) within this time period,
  290.    * we trigger the backoff behavior.
  291.    */
  292.   _serverErrorPeriod: 600000,  // 10 minutes in milliseconds
  293.  
  294.   /**
  295.    * If we get another backoff error immediately after timeout, we increase the
  296.    * backoff to (2 x old period) + this value.
  297.    */
  298.   _serverErrorTimeoutIncrement: 600000,  // 10 minutes in milliseconds
  299.  
  300.   /**
  301.    * The current amount of time to wait before trying a server request
  302.    * after receiving a backoff error.
  303.    */
  304.   _serverErrorTimeout: 0,
  305.  
  306.   /**
  307.    * Time (in unixtime) after which we're allowed to try requesting again.
  308.    */
  309.   _nextRequestTime: 0,
  310.  
  311.   /**
  312.    * The last engine we requested against (so that we can tell if the
  313.    * user switched engines).
  314.    */
  315.   _serverErrorEngine: null,
  316.  
  317.   /**
  318.    * The XMLHttpRequest object.
  319.    * @private
  320.    */
  321.   _request: null,
  322.  
  323.   /**
  324.    * The object implementing nsIAutoCompleteObserver that we notify when
  325.    * we have found results
  326.    * @private
  327.    */
  328.   _listener: null,
  329.  
  330.   /**
  331.    * If this is true, we'll integrate form history results with the
  332.    * suggest results.
  333.    */
  334.   _includeFormHistory: true,
  335.  
  336.   /**
  337.    * True if a request for remote suggestions was sent. This is used to
  338.    * differentiate between the "_request is null because the request has
  339.    * already returned a result" and "_request is null because no request was
  340.    * sent" cases.
  341.    */
  342.   _sentSuggestRequest: false,
  343.  
  344.   /**
  345.    * This is the callback for the suggest timeout timer.
  346.    */
  347.   notify: function SAC_notify(timer) {
  348.     // FIXME: bug 387341
  349.     // Need to break the cycle between us and the timer.
  350.     this._formHistoryTimer = null;
  351.  
  352.     // If this._listener is null, we've already sent out suggest results, so
  353.     // nothing left to do here.
  354.     if (!this._listener)
  355.       return;
  356.  
  357.     // Otherwise, the XMLHTTPRequest for suggest results is taking too long,
  358.     // so send out the form history results and cancel the request.
  359.     this._listener.onSearchResult(this, this._formHistoryResult);
  360.     this._reset();
  361.   },
  362.  
  363.   /**
  364.    * This determines how long (in ms) we should wait before giving up on
  365.    * the suggestions and just showing local form history results.
  366.    */
  367.   _suggestionTimeout: 500,
  368.  
  369.   /**
  370.    * This is the callback for that the form history service uses to
  371.    * send us results.
  372.    */
  373.   onSearchResult: function SAC_onSearchResult(search, result) {
  374.     this._formHistoryResult = result;
  375.  
  376.     if (this._request) {
  377.       // We still have a pending request, wait a bit to give it a chance to
  378.       // finish.
  379.       this._formHistoryTimer = Cc["@mozilla.org/timer;1"].
  380.                                createInstance(Ci.nsITimer);
  381.       this._formHistoryTimer.initWithCallback(this, this._suggestionTimeout,
  382.                                               Ci.nsITimer.TYPE_ONE_SHOT);
  383.     } else if (!this._sentSuggestRequest) {
  384.       // We didn't send a request, so just send back the form history results.
  385.       this._listener.onSearchResult(this, this._formHistoryResult);
  386.       this._reset();
  387.     }
  388.   },
  389.  
  390.   /**
  391.    * This is the URI that the last suggest request was sent to.
  392.    */
  393.   _suggestURI: null,
  394.  
  395.   /**
  396.    * Autocomplete results from the form history service get stored here.
  397.    */
  398.   _formHistoryResult: null,
  399.  
  400.   /**
  401.    * This holds the suggest server timeout timer, if applicable.
  402.    */
  403.   _formHistoryTimer: null,
  404.  
  405.   /**
  406.    * This clears all the per-request state.
  407.    */
  408.   _reset: function SAC_reset() {
  409.     // Don't let go of our listener and form history result if the timer is
  410.     // still pending, the timer will call _reset() when it fires.
  411.     if (!this._formHistoryTimer) {
  412.       this._listener = null;
  413.       this._formHistoryResult = null;
  414.     }
  415.     this._request = null;
  416.   },
  417.  
  418.   /**
  419.    * This sends an autocompletion request to the form history service,
  420.    * which will call onSearchResults with the results of the query.
  421.    */
  422.   _startHistorySearch: function SAC_SHSearch(searchString, searchParam, previousResult) {
  423.     var formHistory =
  424.       Cc["@mozilla.org/autocomplete/search;1?name=form-history"].
  425.       createInstance(Ci.nsIAutoCompleteSearch);
  426.     formHistory.startSearch(searchString, searchParam, previousResult, this);
  427.   },
  428.  
  429.   /**
  430.    * Makes a note of the fact that we've recieved a backoff-triggering
  431.    * response, so that we can adjust the backoff behavior appropriately.
  432.    */
  433.   _noteServerError: function SAC__noteServeError() {
  434.     var currentTime = Date.now();
  435.  
  436.     this._serverErrorLog.push(currentTime);
  437.     if (this._serverErrorLog.length > this._maxErrorsBeforeBackoff)
  438.       this._serverErrorLog.shift();
  439.  
  440.     if ((this._serverErrorLog.length == this._maxErrorsBeforeBackoff) &&
  441.         ((currentTime - this._serverErrorLog[0]) < this._serverErrorPeriod)) {
  442.       // increase timeout, and then don't request until timeout is over
  443.       this._serverErrorTimeout = (this._serverErrorTimeout * 2) +
  444.                                  this._serverErrorTimeoutIncrement;
  445.       this._nextRequestTime = currentTime + this._serverErrorTimeout;
  446.     }
  447.   },
  448.  
  449.   /**
  450.    * Resets the backoff behavior; called when we get a successful response.
  451.    */
  452.   _clearServerErrors: function SAC__clearServerErrors() {
  453.     this._serverErrorLog = [];
  454.     this._serverErrorTimeout = 0;
  455.     this._nextRequestTime = 0;
  456.   },
  457.  
  458.   /**
  459.    * This checks whether we should send a server request (i.e. we're not
  460.    * in a error-triggered backoff period.
  461.    *
  462.    * @private
  463.    */
  464.   _okToRequest: function SAC__okToRequest() {
  465.     return Date.now() > this._nextRequestTime;
  466.   },
  467.  
  468.   /**
  469.    * This checks to see if the new search engine is different
  470.    * from the previous one, and if so clears any error state that might
  471.    * have accumulated for the old engine.
  472.    *
  473.    * @param engine The engine that the suggestion request would be sent to.
  474.    * @private
  475.    */
  476.   _checkForEngineSwitch: function SAC__checkForEngineSwitch(engine) {
  477.     if (engine == this._serverErrorEngine)
  478.       return;
  479.  
  480.     // must've switched search providers, clear old errors
  481.     this._serverErrorEngine = engine;
  482.     this._clearServerErrors();
  483.   },
  484.  
  485.   /**
  486.    * This returns true if the status code of the HTTP response
  487.    * represents a backoff-triggering error.
  488.    *
  489.    * @param status  The status code from the HTTP response
  490.    * @private
  491.    */
  492.   _isBackoffError: function SAC__isBackoffError(status) {
  493.     return ((status == HTTP_INTERNAL_SERVER_ERROR) ||
  494.             (status == HTTP_BAD_GATEWAY) ||
  495.             (status == HTTP_SERVICE_UNAVAILABLE));
  496.   },
  497.  
  498.   /**
  499.    * Called when the 'readyState' of the XMLHttpRequest changes. We only care
  500.    * about state 4 (COMPLETED) - handle the response data.
  501.    * @private
  502.    */
  503.   onReadyStateChange: function() {
  504.     // xxx use the real const here
  505.     if (!this._request || this._request.readyState != 4)
  506.       return;
  507.  
  508.     try {
  509.       var status = this._request.status;
  510.     } catch (e) {
  511.       // The XML HttpRequest can throw NS_ERROR_NOT_AVAILABLE.
  512.       return;
  513.     }
  514.  
  515.     if (this._isBackoffError(status)) {
  516.       this._noteServerError();
  517.       return;
  518.     }
  519.  
  520.     var responseText = this._request.responseText;
  521.     if (status != HTTP_OK || responseText == "")
  522.       return;
  523.  
  524.     this._clearServerErrors();
  525.  
  526.     var serverResults = JSON.fromString(responseText);
  527.     var searchString = serverResults[0] || "";
  528.     var results = serverResults[1] || [];
  529.  
  530.     var comments = [];  // "comments" column values for suggestions
  531.     var historyResults = [];
  532.     var historyComments = [];
  533.  
  534.     // If form history is enabled and has results, add them to the list.
  535.     if (this._includeFormHistory && this._formHistoryResult &&
  536.         (this._formHistoryResult.searchResult ==
  537.          Ci.nsIAutoCompleteResult.RESULT_SUCCESS)) {
  538.       for (var i = 0; i < this._formHistoryResult.matchCount; ++i) {
  539.         var term = this._formHistoryResult.getValueAt(i);
  540.  
  541.         // we don't want things to appear in both history and suggestions
  542.         var dupIndex = results.indexOf(term);
  543.         if (dupIndex != -1)
  544.           results.splice(dupIndex, 1);
  545.  
  546.         historyResults.push(term);
  547.         historyComments.push("");
  548.       }
  549.     }
  550.  
  551.     // fill out the comment column for the suggestions
  552.     for (var i = 0; i < results.length; ++i)
  553.       comments.push("");
  554.  
  555.     // if we have any suggestions, put a label at the top
  556.     if (comments.length > 0)
  557.       comments[0] = this._strings.GetStringFromName("suggestion_label");
  558.  
  559.     // now put the history results above the suggestions
  560.     var finalResults = historyResults.concat(results);
  561.     var finalComments = historyComments.concat(comments);
  562.  
  563.     // Notify the FE of our new results
  564.     this.onResultsReady(searchString, finalResults, finalComments,
  565.                         this._formHistoryResult);
  566.  
  567.     // Reset our state for next time.
  568.     this._reset();
  569.   },
  570.  
  571.   /**
  572.    * Notifies the front end of new results.
  573.    * @param searchString  the user's query string
  574.    * @param results       an array of results to the search
  575.    * @param comments      an array of metadata corresponding to the results
  576.    * @private
  577.    */
  578.   onResultsReady: function(searchString, results, comments,
  579.                            formHistoryResult) {
  580.     if (this._listener) {
  581.       var result = new SuggestAutoCompleteResult(
  582.           searchString,
  583.           Ci.nsIAutoCompleteResult.RESULT_SUCCESS,
  584.           0,
  585.           "",
  586.           results,
  587.           comments,
  588.           formHistoryResult);
  589.  
  590.       this._listener.onSearchResult(this, result);
  591.  
  592.       // Null out listener to make sure we don't notify it twice, in case our
  593.       // timer callback still hasn't run.
  594.       this._listener = null;
  595.     }
  596.   },
  597.  
  598.   /**
  599.    * Initiates the search result gathering process. Part of
  600.    * nsIAutoCompleteSearch implementation.
  601.    *
  602.    * @param searchString    the user's query string
  603.    * @param searchParam     unused, "an extra parameter"; even though
  604.    *                        this parameter and the next are unused, pass
  605.    *                        them through in case the form history
  606.    *                        service wants them
  607.    * @param previousResult  unused, a client-cached store of the previous
  608.    *                        generated resultset for faster searching.
  609.    * @param listener        object implementing nsIAutoCompleteObserver which
  610.    *                        we notify when results are ready.
  611.    */
  612.   startSearch: function(searchString, searchParam, previousResult, listener) {
  613.     var searchService = Cc["@mozilla.org/browser/search-service;1"].
  614.                         getService(Ci.nsIBrowserSearchService);
  615.  
  616.     // If there's an existing request, stop it. There is no smart filtering
  617.     // here as there is when looking through history/form data because the
  618.     // result set returned by the server is different for every typed value -
  619.     // "ocean breathes" does not return a subset of the results returned for
  620.     // "ocean", for example. This does nothing if there is no current request.
  621.     this.stopSearch();
  622.  
  623.     this._listener = listener;
  624.  
  625.     var engine = searchService.currentEngine;
  626.  
  627.     this._checkForEngineSwitch(engine);
  628.  
  629.     if (!searchString ||
  630.         !this._suggestEnabled ||
  631.         !engine.supportsResponseType(SEARCH_RESPONSE_SUGGESTION_JSON) ||
  632.         !this._okToRequest()) {
  633.       // We have an empty search string (user pressed down arrow to see
  634.       // history), or search suggestions are disabled, or the current engine
  635.       // has no suggest functionality, or we're in backoff mode; so just use
  636.       // local history.
  637.       this._sentSuggestRequest = false;
  638.       this._startHistorySearch(searchString, searchParam, previousResult);
  639.       return;
  640.     }
  641.  
  642.     // Actually do the search
  643.     this._request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
  644.                     createInstance(Ci.nsIXMLHttpRequest);
  645.     var submission = engine.getSubmission(searchString,
  646.                                           SEARCH_RESPONSE_SUGGESTION_JSON);
  647.     this._suggestURI = submission.uri;
  648.     var method = (submission.postData ? "POST" : "GET");
  649.     this._request.open(method, this._suggestURI.spec, true);
  650.  
  651.     var self = this;
  652.     function onReadyStateChange() {
  653.       self.onReadyStateChange();
  654.     }
  655.     this._request.onreadystatechange = onReadyStateChange;
  656.     this._request.send(submission.postData);
  657.  
  658.     if (this._includeFormHistory) {
  659.       this._sentSuggestRequest = true;
  660.       this._startHistorySearch(searchString, searchParam, previousResult);
  661.     }
  662.   },
  663.  
  664.   /**
  665.    * Ends the search result gathering process. Part of nsIAutoCompleteSearch
  666.    * implementation.
  667.    */
  668.   stopSearch: function() {
  669.     if (this._request) {
  670.       this._request.abort();
  671.       this._reset();
  672.     }
  673.   },
  674.  
  675.   /**
  676.    * nsIObserver
  677.    */
  678.   observe: function SAC_observe(aSubject, aTopic, aData) {
  679.     switch (aTopic) {
  680.       case NS_PREFBRANCH_PREFCHANGE_TOPIC_ID:
  681.         this._loadSuggestPref();
  682.         break;
  683.       case XPCOM_SHUTDOWN_TOPIC:
  684.         this._removeObservers();
  685.         break;
  686.     }
  687.   },
  688.  
  689.   _addObservers: function SAC_addObservers() {
  690.     var prefService2 = Cc["@mozilla.org/preferences-service;1"].
  691.                        getService(Ci.nsIPrefBranch2);
  692.     prefService2.addObserver(BROWSER_SUGGEST_PREF, this, false);
  693.  
  694.     var os = Cc["@mozilla.org/observer-service;1"].
  695.              getService(Ci.nsIObserverService);
  696.     os.addObserver(this, XPCOM_SHUTDOWN_TOPIC, false);
  697.   },
  698.  
  699.   _removeObservers: function SAC_removeObservers() {
  700.     var prefService2 = Cc["@mozilla.org/preferences-service;1"].
  701.                        getService(Ci.nsIPrefBranch2);
  702.     prefService2.removeObserver(BROWSER_SUGGEST_PREF, this);
  703.  
  704.     var os = Cc["@mozilla.org/observer-service;1"].
  705.              getService(Ci.nsIObserverService);
  706.     os.removeObserver(this, XPCOM_SHUTDOWN_TOPIC);
  707.   },
  708.  
  709.   // nsISupports
  710.   QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompleteSearch,
  711.                                          Ci.nsIAutoCompleteObserver])
  712. };
  713.  
  714. /**
  715.  * SearchSuggestAutoComplete is a service implementation that handles suggest
  716.  * results specific to web searches.
  717.  * @constructor
  718.  */
  719. function SearchSuggestAutoComplete() {
  720.   // This calls _init() in the parent class (SuggestAutoComplete) via the
  721.   // prototype, below.
  722.   this._init();
  723. }
  724. SearchSuggestAutoComplete.prototype = {
  725.   classDescription: "Remote Search Suggestions",
  726.   contractID: "@mozilla.org/autocomplete/search;1?name=search-autocomplete",
  727.   classID: Components.ID("{aa892eb4-ffbf-477d-9f9a-06c995ae9f27}"),
  728.   __proto__: SuggestAutoComplete.prototype,
  729.   serviceURL: ""
  730. };
  731.  
  732. var component = [SearchSuggestAutoComplete];
  733. function NSGetModule(compMgr, fileSpec) {
  734.   return XPCOMUtils.generateModule(component);
  735. }
  736.