home *** CD-ROM | disk | FTP | other *** search
/ Freelog 112 / FreelogNo112-NovembreDecembre2012.iso / Multimedia / Songbird / Songbird_2.0.0-2311_windows-i686-msvc8.exe / xulrunner / components / nsPlacesAutoComplete.js < prev    next >
Text File  |  2010-06-04  |  38KB  |  1,069 lines

  1. /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
  2.  * vim: sw=2 ts=2 sts=2 expandtab
  3.  * ***** BEGIN LICENSE BLOCK *****
  4.  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  5.  *
  6.  * The contents of this file are subject to the Mozilla Public License Version
  7.  * 1.1 (the "License"); you may not use this file except in compliance with
  8.  * the License. You may obtain a copy of the License at
  9.  * http://www.mozilla.org/MPL/
  10.  *
  11.  * Software distributed under the License is distributed on an "AS IS" basis,
  12.  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  13.  * for the specific language governing rights and limitations under the
  14.  * License.
  15.  *
  16.  * The Original Code is mozilla.org code.
  17.  *
  18.  * The Initial Developer of the Original Code is
  19.  * Mozilla Corporation.
  20.  * Portions created by the Initial Developer are Copyright (C) 2008
  21.  * the Initial Developer. All Rights Reserved.
  22.  *
  23.  * Contributor(s):
  24.  *   Shawn Wilsher <me@shawnwilsher.com> (Original Author)
  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. Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
  41.  
  42. ////////////////////////////////////////////////////////////////////////////////
  43. //// Constants
  44.  
  45. const Cc = Components.classes;
  46. const Ci = Components.interfaces;
  47. const Cr = Components.results;
  48.  
  49. // This is just a helper for the next constant.
  50. function book_tag_sql_fragment(aName, aColumn, aForTag)
  51. {
  52.   return ["(",
  53.     "SELECT ", aColumn, " ",
  54.     "FROM moz_bookmarks b ",
  55.     "JOIN moz_bookmarks t ",
  56.       "ON t.id = b.parent ",
  57.       "AND t.parent ", (aForTag ? "" : "!"), "= :parent ",
  58.     "WHERE b.type = ", Ci.nsINavBookmarksService.TYPE_BOOKMARK, " ",
  59.       "AND b.fk = h.id ",
  60.     (aForTag ? "AND LENGTH(t.title) > 0" :
  61.                "ORDER BY b.lastModified DESC LIMIT 1"),
  62.   ") AS ", aName].join("");
  63. }
  64.  
  65. // This SQL query fragment provides the following:
  66. //   - the parent folder for bookmarked entries (kQueryIndexParent)
  67. //   - the bookmark title, if it is a bookmark (kQueryIndexBookmarkTitle)
  68. //   - the tags associated with a bookmarked entry (kQueryIndexTags)
  69. const kBookTagSQLFragment =
  70.   book_tag_sql_fragment("parent", "b.parent", false) + ", " +
  71.   book_tag_sql_fragment("bookmark", "b.title", false) + ", " +
  72.   book_tag_sql_fragment("tags", "GROUP_CONCAT(t.title, ',')", true);
  73.  
  74. // observer topics
  75. const kQuitApplication = "quit-application";
  76. const kPrefChanged = "nsPref:changed";
  77.  
  78. // Match type constants.  These indicate what type of search function we should
  79. // be using.
  80. const MATCH_ANYWHERE = Ci.mozIPlacesAutoComplete.MATCH_ANYWHERE;
  81. const MATCH_BOUNDARY_ANYWHERE = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY_ANYWHERE;
  82. const MATCH_BOUNDARY = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY;
  83. const MATCH_BEGINNING = Ci.mozIPlacesAutoComplete.MATCH_BEGINNING;
  84.  
  85. // AutoComplete index constants.  All AutoComplete queries will provide these
  86. // columns in this order.
  87. const kQueryIndexURL = 0;
  88. const kQueryIndexTitle = 1;
  89. const kQueryIndexFaviconURL = 2;
  90. const kQueryIndexParentId = 3;
  91. const kQueryIndexBookmarkTitle = 4;
  92. const kQueryIndexTags = 5;
  93. const kQueryIndexVisitCount = 6;
  94. const kQueryIndexTyped = 7;
  95. const kQueryIndexPlaceId = 8;
  96. const kQueryIndexQueryType = 9;
  97.  
  98. // AutoComplete query type constants.  Describes the various types of queries
  99. // that we can process.
  100. const kQueryTypeKeyword = 0;
  101. const kQueryTypeFiltered = 1;
  102.  
  103. // This separator is used as an RTL-friendly way to split the title and tags.
  104. // It can also be used by an nsIAutoCompleteResult consumer to re-split the
  105. // "comment" back into the title and the tag.
  106. const kTitleTagsSeparator = " \u2013 ";
  107.  
  108. const kBrowserUrlbarBranch = "browser.urlbar.";
  109.  
  110. ////////////////////////////////////////////////////////////////////////////////
  111. //// Global Functions
  112.  
  113. /**
  114.  * Generates the SQL subquery to get the best favicon for a given revhost.  This
  115.  * is the favicon for the most recent visit.
  116.  *
  117.  * @param aTableName
  118.  *        The table to join to the moz_favicons table with.  This must have a
  119.  *        column called favicon_id.
  120.  * @return the SQL subquery (in string form) to get the best favicon.
  121.  */
  122. function best_favicon_for_revhost(aTableName)
  123. {
  124.   return "(" +
  125.     "SELECT f.url " +
  126.     "FROM " + aTableName + " " +
  127.     "JOIN moz_favicons f ON f.id = favicon_id " +
  128.     "WHERE rev_host = IFNULL( " +
  129.       "(SELECT rev_host FROM moz_places_temp WHERE id = b.fk), " +
  130.       "(SELECT rev_host FROM moz_places WHERE id = b.fk) " +
  131.     ") " +
  132.     "ORDER BY frecency DESC " +
  133.     "LIMIT 1 " +
  134.   ")";
  135. }
  136.  
  137. ////////////////////////////////////////////////////////////////////////////////
  138. //// AutoCompleteStatementCallbackWrapper class
  139.  
  140. /**
  141.  * Wraps a callback and ensures that handleCompletion is not dispatched if the
  142.  * query is no longer tracked.
  143.  *
  144.  * @param aCallback
  145.  *        A reference to a nsPlacesAutoComplete.
  146.  * @param aDBConnection
  147.  *        The database connection to execute the queries on.
  148.  */
  149. function AutoCompleteStatementCallbackWrapper(aCallback,
  150.                                               aDBConnection)
  151. {
  152.   this._callback = aCallback;
  153.   this._db = aDBConnection;
  154. }
  155.  
  156. AutoCompleteStatementCallbackWrapper.prototype = {
  157.   //////////////////////////////////////////////////////////////////////////////
  158.   //// mozIStorageStatementCallback
  159.  
  160.   handleResult: function ACSCW_handleResult(aResultSet)
  161.   {
  162.     this._callback.handleResult.apply(this._callback, arguments);
  163.   },
  164.  
  165.   handleError: function ACSCW_handleError(aError)
  166.   {
  167.     this._callback.handleError.apply(this._callback, arguments);
  168.   },
  169.  
  170.   handleCompletion: function ACSCW_handleCompletion(aReason)
  171.   {
  172.     // Only dispatch handleCompletion if we are not done searching and are a
  173.     // pending search.
  174.     let callback = this._callback;
  175.     if (!callback.isSearchComplete() && callback.isPendingSearch(this._handle))
  176.       callback.handleCompletion.apply(callback, arguments);
  177.   },
  178.  
  179.   //////////////////////////////////////////////////////////////////////////////
  180.   //// AutoCompleteStatementCallbackWrapper
  181.  
  182.   /**
  183.    * Executes the specified query asynchronously.  This object will notify
  184.    * this._callback if we should notify (logic explained in handleCompletion).
  185.    *
  186.    * @param aQueries
  187.    *        The queries to execute asynchronously.
  188.    * @return a mozIStoragePendingStatement that can be used to cancel the
  189.    *         queries.
  190.    */
  191.   executeAsync: function ACSCW_executeAsync(aQueries)
  192.   {
  193.     return this._handle = this._db.executeAsync(aQueries, aQueries.length,
  194.                                                 this);
  195.   },
  196.  
  197.   //////////////////////////////////////////////////////////////////////////////
  198.   //// nsISupports
  199.  
  200.   QueryInterface: XPCOMUtils.generateQI([
  201.     Ci.mozIStorageStatementCallback,
  202.   ])
  203. };
  204.  
  205. ////////////////////////////////////////////////////////////////////////////////
  206. //// nsPlacesAutoComplete class
  207.  
  208. function nsPlacesAutoComplete()
  209. {
  210.   //////////////////////////////////////////////////////////////////////////////
  211.   //// Shared Constants for Smart Getters
  212.  
  213.   // Define common pieces of various queries.
  214.   // TODO bug 412736 in case of a frecency tie, break it with h.typed and
  215.   // h.visit_count which is better than nothing.  This is slow, so not doing it
  216.   // yet...
  217.   // Note: h.frecency is only selected because we need it for ordering.
  218.   function sql_base_fragment(aTableName) {
  219.     return "SELECT h.url, h.title, f.url, " + kBookTagSQLFragment + ", " +
  220.                   "h.visit_count, h.typed, h.id, :query_type, h.frecency " +
  221.            "FROM " + aTableName + " h " +
  222.            "LEFT OUTER JOIN moz_favicons f ON f.id = h.favicon_id " +
  223.            "WHERE h.frecency <> 0 " +
  224.            "AND AUTOCOMPLETE_MATCH(:searchString, h.url, " +
  225.                                   "IFNULL(bookmark, h.title), tags, " +
  226.                                   "h.visit_count, h.typed, parent, " +
  227.                                   ":matchBehavior, :searchBehavior) " +
  228.           "{ADDITIONAL_CONDITIONS} ";
  229.   }
  230.   const SQL_BASE = sql_base_fragment("moz_places_temp") +
  231.                    "UNION ALL " +
  232.                    sql_base_fragment("moz_places") +
  233.                    "AND +h.id NOT IN (SELECT id FROM moz_places_temp) " +
  234.                    "ORDER BY h.frecency DESC, h.id DESC " +
  235.                    "LIMIT :maxResults";
  236.  
  237.   //////////////////////////////////////////////////////////////////////////////
  238.   //// Smart Getters
  239.  
  240.   this.__defineGetter__("_db", function() {
  241.     delete this._db;
  242.     return this._db = Cc["@mozilla.org/browser/nav-history-service;1"].
  243.                       getService(Ci.nsPIPlacesDatabase).
  244.                       DBConnection;
  245.   });
  246.  
  247.   this.__defineGetter__("_bh", function() {
  248.     delete this._bh;
  249.     return this._bh = Cc["@mozilla.org/browser/global-history;2"].
  250.                       getService(Ci.nsIBrowserHistory);
  251.   });
  252.  
  253.   this.__defineGetter__("_textURIService", function() {
  254.     delete this._textURIService;
  255.     return this._textURIService = Cc["@mozilla.org/intl/texttosuburi;1"].
  256.                                   getService(Ci.nsITextToSubURI);
  257.   });
  258.  
  259.   this.__defineGetter__("_bs", function() {
  260.     delete this._bs;
  261.     return this._bs = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
  262.                       getService(Ci.nsINavBookmarksService);
  263.   });
  264.  
  265.   this.__defineGetter__("_ioService", function() {
  266.     delete this._ioService;
  267.     return this._ioService = Cc["@mozilla.org/network/io-service;1"].
  268.                              getService(Ci.nsIIOService);
  269.   });
  270.  
  271.   this.__defineGetter__("_faviconService", function() {
  272.     delete this._faviconService;
  273.     return this._faviconService = Cc["@mozilla.org/browser/favicon-service;1"].
  274.                                   getService(Ci.nsIFaviconService);
  275.   });
  276.  
  277.   this.__defineGetter__("_defaultQuery", function() {
  278.     delete this._defaultQuery;
  279.     let replacementText = "";
  280.     return this._defaultQuery = this._db.createStatement(
  281.       SQL_BASE.replace("{ADDITIONAL_CONDITIONS}", replacementText, "g")
  282.     );
  283.   });
  284.  
  285.   this.__defineGetter__("_historyQuery", function() {
  286.     delete this._historyQuery;
  287.     let replacementText = "AND h.visit_count > 0";
  288.     return this._historyQuery = this._db.createStatement(
  289.       SQL_BASE.replace("{ADDITIONAL_CONDITIONS}", replacementText, "g")
  290.     );
  291.   });
  292.  
  293.   this.__defineGetter__("_bookmarkQuery", function() {
  294.     delete this._bookmarkQuery;
  295.     let replacementText = "AND bookmark IS NOT NULL";
  296.     return this._bookmarkQuery = this._db.createStatement(
  297.       SQL_BASE.replace("{ADDITIONAL_CONDITIONS}", replacementText, "g")
  298.     );
  299.   });
  300.  
  301.   this.__defineGetter__("_tagsQuery", function() {
  302.     delete this._tagsQuery;
  303.     let replacementText = "AND tags IS NOT NULL";
  304.     return this._tagsQuery = this._db.createStatement(
  305.       SQL_BASE.replace("{ADDITIONAL_CONDITIONS}", replacementText, "g")
  306.     );
  307.   });
  308.  
  309.   this.__defineGetter__("_typedQuery", function() {
  310.     delete this._typedQuery;
  311.     let replacementText = "AND h.typed = 1";
  312.     return this._typedQuery = this._db.createStatement(
  313.       SQL_BASE.replace("{ADDITIONAL_CONDITIONS}", replacementText, "g")
  314.     );
  315.   });
  316.  
  317.   this.__defineGetter__("_adaptiveQuery", function() {
  318.     delete this._adaptiveQuery;
  319.     // In this query, we are taking kBookTagSQLFragment only for h.id because it
  320.     // uses data from the moz_bookmarks table and we sync tables on bookmark
  321.     // insert.  So, most likely, h.id will always be populated when we have any
  322.     // bookmark.  We still need to join on moz_places_temp for other data (eg.
  323.     // title).
  324.     return this._adaptiveQuery = this._db.createStatement(
  325.       "/* do not warn (bug 487789) */ " +
  326.       "SELECT IFNULL(h_t.url, h.url) AS c_url, " +
  327.              "IFNULL(h_t.title, h.title) AS c_title, f.url, " +
  328.               kBookTagSQLFragment + ", " +
  329.               "IFNULL(h_t.visit_count, h.visit_count) AS c_visit_count, " +
  330.               "IFNULL(h_t.typed, h.typed) AS c_typed, " +
  331.               "IFNULL(h_t.id, h.id), :query_type, rank " +
  332.       "FROM ( " +
  333.         "SELECT ROUND(MAX(((i.input = :search_string) + " +
  334.                           "(SUBSTR(i.input, 1, LENGTH(:search_string)) = :search_string)) * " +
  335.                           "i.use_count), 1) AS rank, place_id " +
  336.         "FROM moz_inputhistory i " +
  337.         "GROUP BY i.place_id " +
  338.         "HAVING rank > 0 " +
  339.       ") AS i " +
  340.       "LEFT JOIN moz_places h ON h.id = i.place_id " +
  341.       "LEFT JOIN moz_places_temp h_t ON h_t.id = i.place_id " +
  342.       "LEFT JOIN moz_favicons f ON f.id = IFNULL(h_t.favicon_id, h.favicon_id) " + 
  343.       "WHERE c_url NOTNULL " +
  344.       "AND AUTOCOMPLETE_MATCH(:searchString, c_url, " +
  345.                              "IFNULL(bookmark, c_title), tags, " +
  346.                              "c_visit_count, c_typed, parent, " +
  347.                              ":matchBehavior, :searchBehavior) " +
  348.       "ORDER BY rank DESC, IFNULL(h_t.frecency, h.frecency) DESC"
  349.     );
  350.   });
  351.  
  352.   this.__defineGetter__("_keywordQuery", function() {
  353.     delete this._keywordQuery;
  354.     return this._keywordQuery = this._db.createStatement(
  355.       "/* do not warn (bug 487787) */ " +
  356.       "SELECT IFNULL( " +
  357.           "(SELECT REPLACE(url, '%s', :query_string) FROM moz_places_temp WHERE id = b.fk), " +
  358.           "(SELECT REPLACE(url, '%s', :query_string) FROM moz_places WHERE id = b.fk) " +
  359.         ") AS search_url, IFNULL(h_t.title, h.title), " +
  360.         "COALESCE(f.url, " + best_favicon_for_revhost("moz_places_temp") + "," +
  361.                   best_favicon_for_revhost("moz_places") + "), b.parent, " +
  362.         "b.title, NULL, IFNULL(h_t.visit_count, h.visit_count), " +
  363.         "IFNULL(h_t.typed, h.typed), COALESCE(h_t.id, h.id, b.fk), " +
  364.         ":query_type " +
  365.       "FROM moz_keywords k " +
  366.       "JOIN moz_bookmarks b ON b.keyword_id = k.id " +
  367.       "LEFT JOIN moz_places AS h ON h.url = search_url " +
  368.       "LEFT JOIN moz_places_temp AS h_t ON h_t.url = search_url " +
  369.       "LEFT JOIN moz_favicons f ON f.id = IFNULL(h_t.favicon_id, h.favicon_id) " +
  370.       "WHERE LOWER(k.keyword) = LOWER(:keyword) " +
  371.       "ORDER BY IFNULL(h_t.frecency, h.frecency) DESC"
  372.     );
  373.   });
  374.  
  375.   //////////////////////////////////////////////////////////////////////////////
  376.   //// Initialization
  377.  
  378.   // load preferences
  379.   this._prefs = Cc["@mozilla.org/preferences-service;1"].
  380.                 getService(Ci.nsIPrefService).
  381.                 getBranch(kBrowserUrlbarBranch);
  382.   this._loadPrefs(true);
  383.  
  384.   // register observers
  385.   this._os = Cc["@mozilla.org/observer-service;1"].
  386.               getService(Ci.nsIObserverService);
  387.   this._os.addObserver(this, kQuitApplication, false);
  388.  
  389. }
  390.  
  391. nsPlacesAutoComplete.prototype = {
  392.   //////////////////////////////////////////////////////////////////////////////
  393.   //// nsIAutoCompleteSearch
  394.  
  395.   startSearch: function PAC_startSearch(aSearchString, aSearchParam,
  396.                                         aPreviousResult, aListener)
  397.   {
  398.     // Note: We don't use aPreviousResult to make sure ordering of results are
  399.     //       consistent.  See bug 412730 for more details.
  400.  
  401.     // We want to store the original string with no leading or trailing
  402.     // whitespace for case sensitive searches.
  403.     this._originalSearchString = aSearchString.trim();
  404.  
  405.     this._currentSearchString =
  406.       this._fixupSearchText(this._originalSearchString.toLowerCase());
  407.  
  408.     this._listener = aListener;
  409.     let result = Cc["@mozilla.org/autocomplete/simple-result;1"].
  410.                  createInstance(Ci.nsIAutoCompleteSimpleResult);
  411.     result.setSearchString(aSearchString);
  412.     result.setListener(this);
  413.     this._result = result;
  414.  
  415.     // If we are not enabled, we need to return now.
  416.     if (!this._enabled) {
  417.       this._finishSearch(true);
  418.       return;
  419.     }
  420.  
  421.     // Reset our search behavior to the default.
  422.     if (this._currentSearchString)
  423.       this._behavior = this._defaultBehavior;
  424.     else
  425.       this._behavior = this._emptySearchDefaultBehavior;
  426.  
  427.     // For any given search, we run up to three queries:
  428.     // 1) keywords (this._keywordQuery)
  429.     // 2) adaptive learning (this._adaptiveQuery)
  430.     // 3) query from this._getSearch
  431.     // We always run (2) and (3), but (1) only gets ran if we get any filtered
  432.     // tokens from this._getSearch (if there are no tokens, there is nothing to
  433.     // match, so there is no reason to run the query).
  434.     let {query, tokens} =
  435.       this._getSearch(this._getUnfilteredSearchTokens(this._currentSearchString));
  436.     let queries = tokens.length ?
  437.       [this._getBoundKeywordQuery(tokens), this._getBoundAdaptiveQuery(), query] :
  438.       [this._getBoundAdaptiveQuery(), query];
  439.  
  440.     // Start executing our queries.
  441.     this._executeQueries(queries);
  442.  
  443.     // Set up our persistent state for the duration of the search.
  444.     this._searchTokens = tokens;
  445.     this._usedPlaceIds = {};
  446.   },
  447.  
  448.   stopSearch: function PAC_stopSearch()
  449.   {
  450.     // We need to cancel our searches so we do not get any [more] results.
  451.     // However, it's possible we haven't actually started any searches, so this
  452.     // method may throw because this._pendingQuery may be undefined.
  453.     if (this._pendingQuery)
  454.       this._stopActiveQuery();
  455.  
  456.     this._finishSearch(false);
  457.   },
  458.  
  459.   //////////////////////////////////////////////////////////////////////////////
  460.   //// nsIAutoCompleteSimpleResultListener
  461.  
  462.   onValueRemoved: function PAC_onValueRemoved(aResult, aURISpec, aRemoveFromDB)
  463.   {
  464.     if (aRemoveFromDB)
  465.       this._bh.removePage(this._ioService.newURI(aURISpec, null, null));
  466.   },
  467.  
  468.   //////////////////////////////////////////////////////////////////////////////
  469.   //// mozIStorageStatementCallback
  470.  
  471.   handleResult: function PAC_handleResult(aResultSet)
  472.   {
  473.     let row, haveMatches = false;
  474.     while (row = aResultSet.getNextRow()) {
  475.       let match = this._processRow(row);
  476.       haveMatches = haveMatches || match;
  477.  
  478.       if (this._result.matchCount == this._maxRichResults) {
  479.         // We have enough results, so stop running our search.
  480.         this._stopActiveQuery();
  481.  
  482.         // And finish our search.
  483.         this._finishSearch(true);
  484.         return;
  485.       }
  486.  
  487.     }
  488.  
  489.     // Notify about results if we've gotten them.
  490.     if (haveMatches)
  491.       this._notifyResults(true);
  492.   },
  493.  
  494.   handleError: function PAC_handleError(aError)
  495.   {
  496.     Components.utils.reportError("Places AutoComplete: " + aError);
  497.   },
  498.  
  499.   handleCompletion: function PAC_handleCompletion(aReason)
  500.   {
  501.     // If we have already finished our search, we should bail out early.
  502.     if (this.isSearchComplete())
  503.       return;
  504.  
  505.     // If we do not have enough results, and our match type is
  506.     // MATCH_BOUNDARY_ANYWHERE, search again with MATCH_ANYWHERE to get more
  507.     // results.
  508.     if (this._matchBehavior == MATCH_BOUNDARY_ANYWHERE &&
  509.         this._result.matchCount < this._maxRichResults && !this._secondPass) {
  510.       this._secondPass = true;
  511.       let queries = [
  512.         this._getBoundAdaptiveQuery(MATCH_ANYWHERE),
  513.         this._getBoundSearchQuery(MATCH_ANYWHERE, this._searchTokens),
  514.       ];
  515.       this._executeQueries(queries);
  516.       return;
  517.     }
  518.  
  519.     this._finishSearch(true);
  520.   },
  521.  
  522.   //////////////////////////////////////////////////////////////////////////////
  523.   //// nsIObserver
  524.  
  525.   observe: function PAC_observe(aSubject, aTopic, aData)
  526.   {
  527.     if (aTopic == kQuitApplication) {
  528.       this._os.removeObserver(this, kQuitApplication);
  529.  
  530.       // Remove our preference observer.
  531.       this._prefs.removeObserver("", this);
  532.       delete this._prefs;
  533.  
  534.       // Finalize the statements that we have used.
  535.       let stmts = [
  536.         "_defaultQuery",
  537.         "_historyQuery",
  538.         "_bookmarkQuery",
  539.         "_tagsQuery",
  540.         "_typedQuery",
  541.         "_adaptiveQuery",
  542.         "_keywordQuery",
  543.       ];
  544.       for (let i = 0; i < stmts.length; i++) {
  545.         // We do not want to create any query we haven't already created, so
  546.         // see if it is a getter first.  __lookupGetter__ returns null if it is
  547.         // actually a statement.
  548.         if (!this.__lookupGetter__(stmts[i]))
  549.           this[stmts[i]].finalize();
  550.       }
  551.     }
  552.     else if (aTopic == kPrefChanged) {
  553.       this._loadPrefs();
  554.     }
  555.   },
  556.  
  557.   //////////////////////////////////////////////////////////////////////////////
  558.   //// nsPlacesAutoComplete
  559.  
  560.   /**
  561.    * Used to unescape encoded URI strings, and drop information that we do not
  562.    * care about for searching.
  563.    *
  564.    * @param aURIString
  565.    *        The text to unescape and modify.
  566.    * @return the modified uri.
  567.    */
  568.   _fixupSearchText: function PAC_fixupSearchText(aURIString)
  569.   {
  570.     let uri = aURIString;
  571.  
  572.     if (uri.indexOf("http://") == 0)
  573.       uri = uri.slice(7);
  574.     else if (uri.indexOf("https://") == 0)
  575.       uri = uri.slice(8);
  576.     else if (uri.indexOf("ftp://") == 0)
  577.       uri = uri.slice(6);
  578.  
  579.     return this._textURIService.unEscapeURIForUI("UTF-8", uri);
  580.   },
  581.  
  582.   /**
  583.    * Generates the tokens used in searching from a given string.
  584.    *
  585.    * @param aSearchString
  586.    *        The string to generate tokens from.
  587.    * @return an array of tokens.
  588.    */
  589.   _getUnfilteredSearchTokens: function PAC_unfilteredSearchTokens(aSearchString)
  590.   {
  591.     // Calling split on an empty string will return an array containing one
  592.     // empty string.  We don't want that, as it'll break our logic, so return an
  593.     // empty array then.
  594.     return aSearchString.length ? aSearchString.split(" ") : [];
  595.   },
  596.  
  597.   /**
  598.    * Properly cleans up when searching is completed.
  599.    *
  600.    * @param aNotify
  601.    *        Indicates if we should notify the AutoComplete listener about our
  602.    *        results or not.
  603.    */
  604.   _finishSearch: function PAC_finishSearch(aNotify)
  605.   {
  606.     // Notify about results if we are supposed to.
  607.     if (aNotify)
  608.       this._notifyResults(false);
  609.  
  610.     // Clear our state
  611.     delete this._originalSearchString;
  612.     delete this._currentSearchString;
  613.     delete this._searchTokens;
  614.     delete this._listener;
  615.     delete this._result;
  616.     delete this._usedPlaceIds;
  617.     delete this._pendingQuery;
  618.     this._secondPass = false;
  619.   },
  620.  
  621.   /**
  622.    * Executes the given queries asynchronously.
  623.    *
  624.    * @param aQueries
  625.    *        The queries to execute.
  626.    */
  627.   _executeQueries: function PAC_executeQueries(aQueries)
  628.   {
  629.     // Because we might get a handleCompletion for canceled queries, we want to
  630.     // filter out queries we no longer care about (described in the
  631.     // handleCompletion implementation of AutoCompleteStatementCallbackWrapper).
  632.  
  633.     // Create our wrapper object and execute the queries.
  634.     let wrapper = new AutoCompleteStatementCallbackWrapper(this, this._db);
  635.     this._pendingQuery = wrapper.executeAsync(aQueries);
  636.   },
  637.  
  638.   /**
  639.    * Stops executing our active query.
  640.    */
  641.   _stopActiveQuery: function PAC_stopActiveQuery()
  642.   {
  643.     this._pendingQuery.cancel();
  644.     delete this._pendingQuery;
  645.   },
  646.  
  647.   /**
  648.    * Notifies the listener about results.
  649.    *
  650.    * @param aSearchOngoing
  651.    *        Indicates if the search is ongoing or not.
  652.    */
  653.   _notifyResults: function PAC_notifyResults(aSearchOngoing)
  654.   {
  655.     let result = this._result;
  656.     let resultCode = result.matchCount ? "RESULT_SUCCESS" : "RESULT_NOMATCH";
  657.     if (aSearchOngoing)
  658.       resultCode += "_ONGOING";
  659.     result.setSearchResult(Ci.nsIAutoCompleteResult[resultCode]);
  660.     result.setDefaultIndex(result.matchCount ? 0 : -1);
  661.     this._listener.onSearchResult(this, result);
  662.   },
  663.  
  664.   /**
  665.    * Loads the preferences that we care about.
  666.    *
  667.    * @param [optional] aRegisterObserver
  668.    *        Indicates if the preference observer should be added or not.  The
  669.    *        default value is false.
  670.    */
  671.   _loadPrefs: function PAC_loadPrefs(aRegisterObserver)
  672.   {
  673.     let self = this;
  674.     function safeGetter(aName, aDefault) {
  675.       let types = {
  676.         boolean: "Bool",
  677.         number: "Int",
  678.         string: "Char"
  679.       };
  680.       let type = types[typeof(aDefault)];
  681.       if (!type)
  682.         throw "Unknown type!";
  683.  
  684.       // If the pref isn't set, we want to use the default.
  685.       try {
  686.         return self._prefs["get" + type + "Pref"](aName);
  687.       }
  688.       catch (e) {
  689.         return aDefault;
  690.       }
  691.     }
  692.  
  693.     this._enabled = safeGetter("autocomplete.enabled", true);
  694.     this._matchBehavior = safeGetter("matchBehavior", MATCH_BOUNDARY_ANYWHERE);
  695.     this._filterJavaScript = safeGetter("filter.javascript", true);
  696.     this._maxRichResults = safeGetter("maxRichResults", 25);
  697.     this._restrictHistoryToken = safeGetter("restrict.history", "^");
  698.     this._restrictBookmarkToken = safeGetter("restrict.bookmark", "*");
  699.     this._restrictTypedToken = safeGetter("restrict.typed", "~");
  700.     this._restrictTagToken = safeGetter("restrict.tag", "+");
  701.     this._matchTitleToken = safeGetter("match.title", "#");
  702.     this._matchURLToken = safeGetter("match.url", "@");
  703.     this._defaultBehavior = safeGetter("default.behavior", 0);
  704.     // Further restrictions to apply for "empty searches" (i.e. searches for "").
  705.     // By default we use (HISTORY | TYPED) = 33.
  706.     this._emptySearchDefaultBehavior = this._defaultBehavior |
  707.                                        safeGetter("default.behavior.emptyRestriction", 33);
  708.  
  709.     // Validate matchBehavior; default to MATCH_BOUNDARY_ANYWHERE.
  710.     if (this._matchBehavior != MATCH_ANYWHERE &&
  711.         this._matchBehavior != MATCH_BOUNDARY &&
  712.         this._matchBehavior != MATCH_BEGINNING)
  713.       this._matchBehavior = MATCH_BOUNDARY_ANYWHERE;
  714.  
  715.     // register observer
  716.     if (aRegisterObserver) {
  717.       let pb = this._prefs.QueryInterface(Ci.nsIPrefBranch2);
  718.       pb.addObserver("", this, false);
  719.     }
  720.   },
  721.  
  722.   /**
  723.    * Given an array of tokens, this function determines which query should be
  724.    * ran.  It also removes any special search tokens.
  725.    *
  726.    * @param aTokens
  727.    *        An array of search tokens.
  728.    * @return an object with two properties:
  729.    *         query: the correctly optimized, bound query to search the database
  730.    *                with.
  731.    *         tokens: the filtered list of tokens to search with.
  732.    */
  733.   _getSearch: function PAC_getSearch(aTokens)
  734.   {
  735.     // Set the proper behavior so our call to _getBoundSearchQuery gives us the
  736.     // correct query.
  737.     for (let i = aTokens.length - 1; i >= 0; i--) {
  738.       switch (aTokens[i]) {
  739.         case this._restrictHistoryToken:
  740.           this._setBehavior("history");
  741.           break;
  742.         case this._restrictBookmarkToken:
  743.           this._setBehavior("bookmark");
  744.           break;
  745.         case this._restrictTagToken:
  746.           this._setBehavior("tag");
  747.           break;
  748.         case this._matchTitleToken:
  749.           this._setBehavior("title");
  750.           break;
  751.         case this._matchURLToken:
  752.           this._setBehavior("url");
  753.           break;
  754.         case this._restrictTypedToken:
  755.           this._setBehavior("typed");
  756.           break;
  757.         default:
  758.           // We do not want to remove the token if we did not match.
  759.           continue;
  760.       };
  761.  
  762.       aTokens.splice(i, 1);
  763.     }
  764.  
  765.     // Set the right JavaScript behavior based on our preference.  Note that the
  766.     // preference is whether or not we should filter JavaScript, and the
  767.     // behavior is if we should search it or not.
  768.     if (!this._filterJavaScript)
  769.       this._setBehavior("javascript");
  770.  
  771.     return {
  772.       query: this._getBoundSearchQuery(this._matchBehavior, aTokens),
  773.       tokens: aTokens
  774.     };
  775.   },
  776.  
  777.   /**
  778.    * Obtains the search query to be used based on the previously set search
  779.    * behaviors (accessed by this._hasBehavior).  The query is bound and ready to
  780.    * execute.
  781.    *
  782.    * @param aMatchBehavior
  783.    *        How this query should match its tokens to the search string.
  784.    * @param aTokens
  785.    *        An array of search tokens.
  786.    * @return the correctly optimized query to search the database with and the
  787.    *         new list of tokens to search with.  The query has all the needed
  788.    *         parameters bound, so consumers can execute it without doing any
  789.    *         additional work.
  790.    */
  791.   _getBoundSearchQuery: function PAC_getBoundSearchQuery(aMatchBehavior,
  792.                                                          aTokens)
  793.   {
  794.     // We use more optimized queries for restricted searches, so we will always
  795.     // return the most restrictive one to the least restrictive one if more than
  796.     // one token is found.
  797.     let query = this._hasBehavior("tag") ? this._tagsQuery :
  798.                 this._hasBehavior("bookmark") ? this._bookmarkQuery :
  799.                 this._hasBehavior("typed") ? this._typedQuery :
  800.                 this._hasBehavior("history") ? this._historyQuery :
  801.                 this._defaultQuery;
  802.  
  803.     // Bind the needed parameters to the query so consumers can use it.
  804.     let (params = query.params) {
  805.       params.parent = this._bs.tagsFolder;
  806.       params.query_type = kQueryTypeFiltered;
  807.       params.matchBehavior = aMatchBehavior;
  808.       params.searchBehavior = this._behavior;
  809.  
  810.       // We only want to search the tokens that we are left with - not the
  811.       // original search string.
  812.       params.searchString = aTokens.join(" ");
  813.  
  814.       // Limit the query to the the maximum number of desired results.
  815.       // This way we can avoid doing more work than needed.
  816.       params.maxResults = this._maxRichResults;
  817.     }
  818.  
  819.     return query;
  820.   },
  821.  
  822.   /**
  823.    * Obtains the keyword query with the properly bound parameters.
  824.    *
  825.    * @param aTokens
  826.    *        The array of search tokens to check against.
  827.    * @return the bound keyword query.
  828.    */
  829.   _getBoundKeywordQuery: function PAC_getBoundKeywordQuery(aTokens)
  830.   {
  831.     // The keyword is the first word in the search string, with the parameters
  832.     // following it.
  833.     let searchString = this._originalSearchString;
  834.     let queryString = searchString.substring(searchString.indexOf(" ") + 1);
  835.  
  836.     // We need to escape the parameters as if they were the query in a URL
  837.     queryString = encodeURIComponent(queryString).replace("%20", "+", "g");
  838.  
  839.     // The first word could be a keyword, so that's what we'll search.
  840.     let keyword = aTokens[0];
  841.  
  842.     let query = this._keywordQuery;
  843.     let (params = query.params) {
  844.       params.keyword = keyword;
  845.       params.query_string = queryString;
  846.       params.query_type = kQueryTypeKeyword;
  847.     }
  848.  
  849.     return query;
  850.   },
  851.  
  852.   /**
  853.    * Obtains the adaptive query with the properly bound parameters.
  854.    *
  855.    * @return the bound adaptive query.
  856.    */
  857.   _getBoundAdaptiveQuery: function PAC_getBoundAdaptiveQuery(aMatchBehavior)
  858.   {
  859.     // If we were not given a match behavior, use the stored match behavior.
  860.     if (arguments.length == 0)
  861.       aMatchBehavior = this._matchBehavior;
  862.  
  863.     let query = this._adaptiveQuery;
  864.     let (params = query.params) {
  865.       params.parent = this._bs.tagsFolder;
  866.       params.search_string = this._currentSearchString;
  867.       params.query_type = kQueryTypeFiltered;
  868.       params.matchBehavior = aMatchBehavior;
  869.       params.searchBehavior = this._behavior;
  870.     }
  871.  
  872.     return query;
  873.   },
  874.  
  875.   /**
  876.    * Processes a mozIStorageRow to generate the proper data for the AutoComplete
  877.    * result.  This will add an entry to the current result if it matches the
  878.    * criteria.
  879.    *
  880.    * @param aRow
  881.    *        The row to process.
  882.    * @return true if the row is accepted, and false if not.
  883.    */
  884.   _processRow: function PAC_processRow(aRow)
  885.   {
  886.     // Before we do any work, make sure this entry isn't already in our results.
  887.     let entryId = aRow.getResultByIndex(kQueryIndexPlaceId);
  888.     if (this._inResults(entryId))
  889.       return false;
  890.  
  891.     let escapedEntryURL = aRow.getResultByIndex(kQueryIndexURL);
  892.     let entryTitle = aRow.getResultByIndex(kQueryIndexTitle) || "";
  893.     let entryFavicon = aRow.getResultByIndex(kQueryIndexFaviconURL) || "";
  894.     let entryParentId = aRow.getResultByIndex(kQueryIndexParentId);
  895.     let entryBookmarkTitle = entryParentId ?
  896.       aRow.getResultByIndex(kQueryIndexBookmarkTitle) : null;
  897.     let entryTags = aRow.getResultByIndex(kQueryIndexTags) || "";
  898.  
  899.     // Always prefer the bookmark title unless it is empty
  900.     let title = entryBookmarkTitle || entryTitle;
  901.  
  902.     let style;
  903.     if (aRow.getResultByIndex(kQueryIndexQueryType) == kQueryTypeKeyword) {
  904.       // If we do not have a title, then we must have a keyword, so let the UI
  905.       // know it is a keyword.  Otherwise, we found an exact page match, so just
  906.       // show the page like a regular result.  Because the page title is likely
  907.       // going to be more specific than the bookmark title (keyword title).
  908.       if (!entryTitle)
  909.         style = "keyword";
  910.       else
  911.         title = entryTitle;
  912.     }
  913.  
  914.     // We will always prefer to show tags if we have them.
  915.     let showTags = !!entryTags;
  916.  
  917.     // However, we'll act as if a page is not bookmarked or tagged if the user
  918.     // only wants only history and not bookmarks or tags.
  919.     if (this._hasBehavior("history") &&
  920.         !(this._hasBehavior("bookmark") || this._hasBehavior("tag"))) {
  921.       showTags = false;
  922.       style = "favicon";
  923.     }
  924.  
  925.     // If we have tags and should show them, we need to add them to the title.
  926.     if (showTags)
  927.       title += kTitleTagsSeparator + entryTags;
  928.  
  929.     // We have to determine the right style to display.  Tags show the tag icon,
  930.     // bookmarks get the bookmark icon, and keywords get the keyword icon.  If
  931.     // the result does not fall into any of those, it just gets the favicon.
  932.     if (!style) {
  933.       // It is possible that we already have a style set (from a keyword
  934.       // search or because of the user's preferences), so only set it if we
  935.       // haven't already done so.
  936.       if (showTags)
  937.         style = "tag";
  938.       else if (entryParentId)
  939.         style = "bookmark";
  940.       else
  941.         style = "favicon";
  942.     }
  943.  
  944.     // And finally add this to our results.
  945.     this._addToResults(entryId, escapedEntryURL, title, entryFavicon, style);
  946.     return true;
  947.   },
  948.  
  949.   /**
  950.    * Checks to see if the given place has already been added to the results.
  951.    *
  952.    * @param aPlaceId
  953.    *        The place_id to check for.
  954.    * @return true if the place has been added, false otherwise.
  955.    */
  956.   _inResults: function PAC_inResults(aPlaceId)
  957.   {
  958.     return (aPlaceId in this._usedPlaceIds);
  959.   },
  960.  
  961.   /**
  962.    * Adds a result to the AutoComplete results.  Also tracks that we've added
  963.    * this place_id into the result set.
  964.    *
  965.    * @param aPlaceId
  966.    *        The place_id of the item to be added to the result set.  This is
  967.    *        used by _inResults.
  968.    * @param aURISpec
  969.    *        The URI spec for the entry.
  970.    * @param aTitle
  971.    *        The title to give the entry.
  972.    * @param aFaviconSpec
  973.    *        The favicon to give to the entry.
  974.    * @param aStyle
  975.    *        Indicates how the entry should be styled when displayed.
  976.    */
  977.   _addToResults: function PAC_addToResults(aPlaceId, aURISpec, aTitle,
  978.                                            aFaviconSpec, aStyle)
  979.   {
  980.     // Add this to our internal tracker to ensure duplicates do not end up in
  981.     // the result.  _usedPlaceIds is an Object that is being used as a set.
  982.     this._usedPlaceIds[aPlaceId] = true;
  983.  
  984.     // Obtain the favicon for this URI.
  985.     let favicon;
  986.     if (aFaviconSpec) {
  987.       let uri = this._ioService.newURI(aFaviconSpec, null, null);
  988.       favicon = this._faviconService.getFaviconLinkForIcon(uri).spec;
  989.     }
  990.     favicon = favicon || this._faviconService.defaultFavicon.spec;
  991.  
  992.     this._result.appendMatch(aURISpec, aTitle, favicon, aStyle);
  993.   },
  994.  
  995.   /**
  996.    * Determines if the specified AutoComplete behavior is set.
  997.    *
  998.    * @param aType
  999.    *        The behavior type to test for.
  1000.    * @return true if the behavior is set, false otherwise.
  1001.    */
  1002.   _hasBehavior: function PAC_hasBehavior(aType)
  1003.   {
  1004.     return (this._behavior &
  1005.             Ci.mozIPlacesAutoComplete["BEHAVIOR_" + aType.toUpperCase()]);
  1006.   },
  1007.  
  1008.   /**
  1009.    * Enables the desired AutoComplete behavior.
  1010.    *
  1011.    * @param aType
  1012.    *        The behavior type to set.
  1013.    */
  1014.   _setBehavior: function PAC_setBehavior(aType)
  1015.   {
  1016.     this._behavior |=
  1017.       Ci.mozIPlacesAutoComplete["BEHAVIOR_" + aType.toUpperCase()];
  1018.   },
  1019.  
  1020.   /**
  1021.    * Determines if we are done searching or not.
  1022.    *
  1023.    * @return true if we have completed searching, false otherwise.
  1024.    */
  1025.   isSearchComplete: function PAC_isSearchComplete()
  1026.   {
  1027.     // If _pendingQuery is null, we should no longer do any work since we have
  1028.     // already called _finishSearch.  This means we completed our search.
  1029.     return this._pendingQuery == null;
  1030.   },
  1031.  
  1032.   /**
  1033.    * Determines if the given handle of a pending statement is a pending search
  1034.    * or not.
  1035.    *
  1036.    * @param aHandle
  1037.    *        A mozIStoragePendingStatement to check and see if we are waiting for
  1038.    *        results from it still.
  1039.    * @return true if it is a pending query, false otherwise.
  1040.    */
  1041.   isPendingSearch: function PAC_isPendingSearch(aHandle)
  1042.   {
  1043.     return this._pendingQuery == aHandle;
  1044.   },
  1045.  
  1046.   //////////////////////////////////////////////////////////////////////////////
  1047.   //// nsISupports
  1048.  
  1049.   classDescription: "AutoComplete result generator for Places.",
  1050.   classID: Components.ID("d0272978-beab-4adc-a3d4-04b76acfa4e7"),
  1051.   contractID: "@mozilla.org/autocomplete/search;1?name=history",
  1052.  
  1053.   QueryInterface: XPCOMUtils.generateQI([
  1054.     Ci.nsIAutoCompleteSearch,
  1055.     Ci.nsIAutoCompleteSimpleResultListener,
  1056.     Ci.mozIStorageStatementCallback,
  1057.     Ci.nsIObserver,
  1058.   ])
  1059. };
  1060.  
  1061. ////////////////////////////////////////////////////////////////////////////////
  1062. //// Module Registration
  1063.  
  1064. let components = [nsPlacesAutoComplete];
  1065. function NSGetModule(compMgr, fileSpec)
  1066. {
  1067.   return XPCOMUtils.generateModule(components);
  1068. }
  1069.